Skip to main content

content_security_policy/
lib.rs

1/*!
2Parse and validate Web [Content-Security-Policy level 3](https://www.w3.org/TR/CSP/)
3
4# Example
5
6```rust
7extern crate content_security_policy;
8use content_security_policy::*;
9fn main() {
10    let csp_list = CspList::parse("script-src *.notriddle.com", PolicySource::Header, PolicyDisposition::Enforce);
11    let (check_result, _) = csp_list.should_request_be_blocked(&Request {
12        url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
13        current_url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
14        origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
15        redirect_count: 0,
16        destination: Destination::Script,
17        initiator: Initiator::None,
18        nonce: String::new(),
19        integrity_metadata: String::new(),
20        parser_metadata: ParserMetadata::None,
21    });
22    assert_eq!(check_result, CheckResult::Allowed);
23    let (check_result, _) = csp_list.should_request_be_blocked(&Request {
24        url: Url::parse("https://www.evil.example/script.js").unwrap(),
25        current_url: Url::parse("https://www.evil.example/script.js").unwrap(),
26        origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
27        redirect_count: 0,
28        destination: Destination::Script,
29        initiator: Initiator::None,
30        nonce: String::new(),
31        integrity_metadata: String::new(),
32        parser_metadata: ParserMetadata::None,
33    });
34    assert_eq!(check_result, CheckResult::Blocked);
35}
36```
37*/
38
39#![forbid(unsafe_code)]
40
41pub extern crate percent_encoding;
42pub extern crate url;
43
44pub mod sandboxing_directive;
45pub(crate) mod text_util;
46
47use once_cell::sync::Lazy;
48use regex::Regex;
49use sandboxing_directive::{parse_a_sandboxing_directive, SandboxingFlagSet};
50#[cfg(feature = "serde")]
51use serde::{Deserialize, Serialize};
52use sha2::Digest;
53use std::borrow::{Borrow, Cow};
54use std::cmp;
55use std::collections::HashSet;
56use std::fmt::{self, Display, Formatter};
57use std::str::FromStr;
58use text_util::{
59    ascii_case_insensitive_match, collect_a_sequence_of_non_ascii_white_space_code_points,
60    split_ascii_whitespace, split_commas, strip_leading_and_trailing_ascii_whitespace,
61};
62pub use url::{Origin, Position, Url};
63use MatchResult::DoesNotMatch;
64use MatchResult::Matches;
65
66fn scheme_is_network(scheme: &str) -> bool {
67    scheme == "ftp" || scheme_is_httpx(scheme)
68}
69
70fn scheme_is_httpx(scheme: &str) -> bool {
71    scheme == "http" || scheme == "https"
72}
73
74/**
75A single parsed content security policy.
76
77https://www.w3.org/TR/CSP/#content-security-policy-object
78*/
79#[derive(Clone, Debug)]
80#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
81pub struct Policy {
82    pub directive_set: Vec<Directive>,
83    pub disposition: PolicyDisposition,
84    pub source: PolicySource,
85}
86
87impl Display for Policy {
88    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
89        for (i, directive) in self.directive_set.iter().enumerate() {
90            if i != 0 {
91                write!(f, "; ")?;
92            }
93            <Directive as Display>::fmt(directive, f)?;
94        }
95        Ok(())
96    }
97}
98
99impl Policy {
100    pub fn is_valid(&self) -> bool {
101        self.directive_set.iter().all(Directive::is_valid)
102            && self
103                .directive_set
104                .iter()
105                .map(|d| d.name.clone())
106                .collect::<HashSet<_>>()
107                .len()
108                == self.directive_set.len()
109            && !self.directive_set.is_empty()
110    }
111    /// https://www.w3.org/TR/CSP/#parse-serialized-policy
112    pub fn parse(serialized: &str, source: PolicySource, disposition: PolicyDisposition) -> Policy {
113        let mut policy = Policy {
114            directive_set: Vec::new(),
115            source,
116            disposition,
117        };
118        // Rust's str::split corresponds to a WHATWG "strict split"
119        for token in serialized.split(';') {
120            let token = strip_leading_and_trailing_ascii_whitespace(token);
121            if token.is_empty() {
122                continue;
123            };
124            let (directive_name, token) =
125                collect_a_sequence_of_non_ascii_white_space_code_points(token);
126            let mut directive_name = directive_name.to_owned();
127            directive_name.make_ascii_lowercase();
128            if policy.contains_a_directive_whose_name_is(&directive_name) {
129                continue;
130            }
131            let directive_value = split_ascii_whitespace(token).map(String::from).collect();
132            policy.directive_set.push(Directive {
133                name: directive_name,
134                value: directive_value,
135            });
136        }
137        policy
138    }
139    pub fn contains_a_directive_whose_name_is(&self, directive_name: &str) -> bool {
140        self.directive_set.iter().any(|d| d.name == directive_name)
141    }
142    /// https://www.w3.org/TR/CSP/#does-request-violate-policy
143    pub fn does_request_violate_policy(&self, request: &Request) -> Violates {
144        if request.initiator == Initiator::Prefetch {
145            return self.does_resource_hint_violate_policy(request);
146        }
147
148        let mut violates = Violates::DoesNotViolate;
149        for directive in &self.directive_set {
150            let result = directive.pre_request_check(request, self);
151            if result == CheckResult::Blocked {
152                violates = Violates::Directive(directive.clone());
153            }
154        }
155        violates
156    }
157
158    /// https://www.w3.org/TR/CSP/#does-resource-hint-violate-policy
159    pub fn does_resource_hint_violate_policy(&self, request: &Request) -> Violates {
160        let default_directive = &self.directive_set.iter().find(|x| x.name == "default-src");
161
162        if default_directive.is_none() {
163            return Violates::DoesNotViolate;
164        }
165
166        for directive in &self.directive_set {
167            let result = directive.pre_request_check(request, self);
168            if result == CheckResult::Allowed {
169                return Violates::DoesNotViolate;
170            }
171        }
172
173        return Violates::Directive(default_directive.unwrap().clone());
174    }
175}
176
177#[derive(Clone, Debug)]
178#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
179/// https://www.w3.org/TR/CSP/#csp-list
180pub struct CspList(pub Vec<Policy>);
181
182impl Display for CspList {
183    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
184        for (i, directive) in self.0.iter().enumerate() {
185            if i != 0 {
186                write!(f, ",")?;
187            }
188            <Policy as Display>::fmt(directive, f)?;
189        }
190        Ok(())
191    }
192}
193
194/// https://www.w3.org/TR/trusted-types/#trusted-types-csp-directive
195static TRUSTED_POLICY_SOURCE_GRAMMAR: Lazy<Regex> =
196    Lazy::new(|| Regex::new(r#"^[0-9a-zA-Z\-\#=_\/@\.%]+$"#).unwrap());
197
198impl CspList {
199    pub fn is_valid(&self) -> bool {
200        self.0.iter().all(Policy::is_valid)
201    }
202    /// https://www.w3.org/TR/CSP/#contains-a-header-delivered-content-security-policy
203    pub fn contains_a_header_delivered_content_security_policy(&self) -> bool {
204        self.0
205            .iter()
206            .any(|policy| policy.source == PolicySource::Header)
207    }
208    /// https://www.w3.org/TR/CSP/#parse-serialized-policy-list
209    pub fn parse(list: &str, source: PolicySource, disposition: PolicyDisposition) -> CspList {
210        let mut policies = Vec::new();
211        for token in split_commas(list) {
212            let policy = Policy::parse(token, source, disposition);
213            if policy.directive_set.is_empty() {
214                continue;
215            };
216            policies.push(policy)
217        }
218        CspList(policies)
219    }
220    pub fn append(&mut self, mut other: CspList) {
221        self.0.append(&mut other.0)
222    }
223    pub fn push(&mut self, policy: Policy) {
224        self.0.push(policy)
225    }
226    /**
227    Given a request, this algorithm reports violations based on client’s "report only" policies.
228
229    https://www.w3.org/TR/CSP/#report-for-request
230    */
231    pub fn report_violations_for_request(&self, request: &Request) -> Vec<Violation> {
232        let mut violations = Vec::new();
233        for policy in &self.0 {
234            if policy.disposition == PolicyDisposition::Enforce {
235                continue;
236            };
237            let violates = policy.does_request_violate_policy(request);
238            if let Violates::Directive(directive) = violates {
239                let resource = ViolationResource::Url(request.url.clone());
240                violations.push(Violation {
241                    resource,
242                    directive: Directive {
243                        name: get_the_effective_directive_for_request(request).to_owned(),
244                        value: directive.value.clone(),
245                    },
246                    policy: policy.clone(),
247                });
248            }
249        }
250        violations
251    }
252    /**
253    Given a request, this algorithm returns Blocked or Allowed and reports violations based on
254    request’s client’s Content Security Policy.
255
256    https://www.w3.org/TR/CSP/#should-block-request
257    */
258    pub fn should_request_be_blocked(&self, request: &Request) -> (CheckResult, Vec<Violation>) {
259        let mut result = CheckResult::Allowed;
260        let mut violations = Vec::new();
261        for policy in &self.0 {
262            if policy.disposition == PolicyDisposition::Report {
263                continue;
264            };
265            let violates = policy.does_request_violate_policy(request);
266            if let Violates::Directive(directive) = violates {
267                result = CheckResult::Blocked;
268                let resource = ViolationResource::Url(request.url.clone());
269                violations.push(Violation {
270                    resource,
271                    directive: Directive {
272                        name: get_the_effective_directive_for_request(request).to_owned(),
273                        value: directive.value.clone(),
274                    },
275                    policy: policy.clone(),
276                });
277            }
278        }
279        (result, violations)
280    }
281    /**
282    Given a response and a request, this algorithm returns Blocked or Allowed, and reports
283    violations based on request’s client’s Content Security Policy.
284
285    https://www.w3.org/TR/CSP/#should-block-response
286    */
287    pub fn should_response_to_request_be_blocked(
288        &self,
289        request: &Request,
290        response: &Response,
291    ) -> (CheckResult, Vec<Violation>) {
292        // Step 1. Let CSP list be request’s policy container’s CSP list.
293        // step 2. Let result be "Allowed".
294        let mut result = CheckResult::Allowed;
295        let mut violations = Vec::new();
296        // Step 3. For each policy of CSP list:
297        for policy in &self.0 {
298            // Step 3.1. For each directive of policy:
299            for directive in &policy.directive_set {
300                // Step 3.1.1. If the result of executing directive’s post-request check is "Blocked", then:
301                if directive.post_request_check(request, response, policy) == CheckResult::Blocked {
302                    // Step 3.1.1.1. Execute §5.5 Report a violation on the result of executing
303                    // §2.4.2 Create a violation object for request, and policy. on request, and policy.
304                    violations.push(Violation {
305                        resource: ViolationResource::Url(request.url.clone()),
306                        directive: Directive {
307                            name: get_the_effective_directive_for_request(request).to_owned(),
308                            value: directive.value.clone(),
309                        },
310                        policy: policy.clone(),
311                    });
312                    // Step 3.1.1.2. If policy’s disposition is "enforce", then set result to "Blocked".
313                    if policy.disposition == PolicyDisposition::Enforce {
314                        result = CheckResult::Blocked;
315                    }
316                }
317            }
318        }
319        (result, violations)
320    }
321    /// https://www.w3.org/TR/CSP/#should-block-inline
322    pub fn should_elements_inline_type_behavior_be_blocked(
323        &self,
324        element: &Element,
325        type_: InlineCheckType,
326        source: &str,
327    ) -> (CheckResult, Vec<Violation>) {
328        use CheckResult::*;
329        let mut result = Allowed;
330        let mut violations = Vec::new();
331        for policy in &self.0 {
332            for directive in &policy.directive_set {
333                if directive.inline_check(element, type_, policy, source) == Allowed {
334                    continue;
335                }
336                let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
337                    let max_length = cmp::min(40, source.len());
338                    Some(source[0..max_length].to_owned())
339                } else {
340                    None
341                };
342                let violation = Violation {
343                    resource: ViolationResource::Inline { sample },
344                    directive: Directive {
345                        name: get_the_effective_directive_for_inline_checks(type_).to_owned(),
346                        value: directive.value.clone(),
347                    },
348                    policy: policy.clone(),
349                };
350                violations.push(violation);
351                if policy.disposition == PolicyDisposition::Enforce {
352                    result = Blocked;
353                }
354            }
355        }
356        (result, violations)
357    }
358    /**
359    https://www.w3.org/TR/CSP/#allow-base-for-document
360
361    Note that, while this algoritm is defined as operating on a document, the only property it
362    actually uses is the document's CSP List. So this function operates on that.
363    */
364    pub fn is_base_allowed_for_document(
365        &self,
366        base: &Url,
367        self_origin: &Origin,
368    ) -> (CheckResult, Vec<Violation>) {
369        use CheckResult::*;
370        let mut violations = Vec::new();
371        for policy in &self.0 {
372            let directive = policy
373                .directive_set
374                .iter()
375                .find(|directive| directive.name == "base-uri");
376            if let Some(directive) = directive {
377                if SourceList(&directive.value)
378                    .does_url_match_source_list_in_origin_with_redirect_count(base, &self_origin, 0)
379                    == DoesNotMatch
380                {
381                    let violation = Violation {
382                        directive: directive.clone(),
383                        resource: ViolationResource::Inline { sample: None },
384                        policy: policy.clone(),
385                    };
386                    violations.push(violation);
387                    if policy.disposition == PolicyDisposition::Enforce {
388                        return (Blocked, violations);
389                    }
390                }
391            }
392        }
393        return (Allowed, violations);
394    }
395
396    /**
397    https://w3c.github.io/trusted-types/dist/spec/#should-block-create-policy
398
399    Note that, while this algoritm is defined as operating on a global object, the only property it
400    actually uses is the global's CSP List. So this function operates on that.
401    */
402    pub fn is_trusted_type_policy_creation_allowed(
403        &self,
404        policy_name: &str,
405        created_policy_names: &[&str],
406    ) -> (CheckResult, Vec<Violation>) {
407        use CheckResult::*;
408        // Step 1: Let result be "Allowed".
409        let mut result = Allowed;
410        let mut violations = Vec::new();
411        // Step 2: For each policy in global’s CSP list:
412        for policy in &self.0 {
413            // Step 2.1: Let createViolation be false.
414            let mut create_violation = false;
415            // Step 2.2: If policy’s directive set does not contain a directive which name is "trusted-types", skip to the next policy.
416            let directive = policy
417                .directive_set
418                .iter()
419                .find(|directive| directive.name == "trusted-types");
420            // Step 2.3: Let directive be the policy’s directive set’s directive which name is "trusted-types"
421            if let Some(directive) = directive {
422                // Step 2.4: If directive’s value only contains a tt-keyword which is a match for a value 'none', set createViolation to true.
423                if directive.value.len() == 1 && directive.value.contains(&"'none'".to_string()) {
424                    create_violation = true;
425                }
426                // Step 2.5: If createdPolicyNames contains policyName and directive’s value does not contain a tt-keyword
427                // which is a match for a value 'allow-duplicates', set createViolation to true.
428                if created_policy_names.contains(&policy_name)
429                    && !directive.value.iter().any(|v| v == "'allow-duplicates'")
430                {
431                    create_violation = true;
432                }
433                // Step 2.6: If directive’s value does not contain a tt-policy-name, which value is policyName,
434                // and directive’s value does not contain a tt-wildcard, set createViolation to true.
435                if !(TRUSTED_POLICY_SOURCE_GRAMMAR.is_match(&policy_name)
436                    && (directive.value.iter().any(|p| p == policy_name)
437                        || directive.value.iter().any(|v| v == "*")))
438                {
439                    create_violation = true;
440                }
441                // Step 2.7: If createViolation is false, skip to the next policy.
442                if !create_violation {
443                    continue;
444                }
445                let max_length = cmp::min(40, policy_name.len());
446                // Step 2.10: Set violation’s sample to the substring of policyName, containing its first 40 characters.
447                let sample = policy_name[0..max_length].to_owned();
448                // Step 2.8: Let violation be the result of executing Create a violation object for global, policy,
449                // and directive on global, policy and "trusted-types"
450                let violation = Violation {
451                    directive: directive.clone(),
452                    // Step 2.9: Set violation’s resource to "trusted-types-policy".
453                    resource: ViolationResource::TrustedTypePolicy {
454                        // Step 2.10: Set violation’s sample to the substring of policyName, containing its first 40 characters.
455                        sample,
456                    },
457                    policy: policy.clone(),
458                };
459                // Step 2.11: Execute Report a violation on violation.
460                violations.push(violation);
461                // Step 2.12: If policy’s disposition is "enforce", then set result to "Blocked".
462                if policy.disposition == PolicyDisposition::Enforce {
463                    result = Blocked
464                }
465            }
466        }
467        return (result, violations);
468    }
469    /**
470    https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-does-sink-type-require-trusted-types
471
472    Note that, while this algoritm is defined as operating on a global object, the only property it
473    actually uses is the global's CSP List. So this function operates on that.
474    */
475    pub fn does_sink_type_require_trusted_types(
476        &self,
477        sink_group: &str,
478        include_report_only_policies: bool,
479    ) -> bool {
480        let sink_group = &sink_group.to_owned();
481        // Step 1: For each policy in global’s CSP list:
482        for policy in &self.0 {
483            // Step 1.1: If policy’s directive set does not contain a directive whose name is "require-trusted-types-for", skip to the next policy.
484            let directive = policy
485                .directive_set
486                .iter()
487                .find(|directive| directive.name == "require-trusted-types-for");
488            // Step 1.2: Let directive be the policy’s directive set’s directive whose name is "require-trusted-types-for"
489            if let Some(directive) = directive {
490                // Step 1.3: If directive’s value does not contain a trusted-types-sink-group which is a match for sinkGroup, skip to the next policy.
491                if !directive.value.contains(sink_group) {
492                    continue;
493                }
494                // Step 1.4: Let enforced be true if policy’s disposition is "enforce", and false otherwise.
495                let enforced = policy.disposition == PolicyDisposition::Enforce;
496                // Step 1.5: If enforced is true, return true.
497                if enforced {
498                    return true;
499                }
500                // Step 1.6: If includeReportOnlyPolicies is true, return true.
501                if include_report_only_policies {
502                    return true;
503                }
504            }
505        }
506        // Step 2: Return false.
507        false
508    }
509    /**
510    https://w3c.github.io/trusted-types/dist/spec/#should-block-sink-type-mismatch
511
512    Note that, while this algoritm is defined as operating on a global object, the only property it
513    actually uses is the global's CSP List. So this function operates on that.
514    */
515    pub fn should_sink_type_mismatch_violation_be_blocked_by_csp(
516        &self,
517        sink: &str,
518        sink_group: &str,
519        source: &str,
520    ) -> (CheckResult, Vec<Violation>) {
521        use CheckResult::*;
522        let sink_group = &sink_group.to_owned();
523        // Step 1: Let result be "Allowed".
524        let mut result = Allowed;
525        let mut violations = Vec::new();
526        // Step 2: Let sample be source.
527        let mut sample = source;
528        // Step 3: If sink is "Function", then:
529        if sink == "Function" {
530            // Step 3.1: If sample starts with "function anonymous", strip that from sample.
531            if sample.starts_with("function anonymous") {
532                sample = &sample[18..];
533                // Step 3.2: Otherwise if sample starts with "async function anonymous", strip that from sample.
534            } else if sample.starts_with("async function anonymous") {
535                sample = &sample[24..];
536                // Step 3.3: Otherwise if sample starts with "function* anonymous", strip that from sample.
537            } else if sample.starts_with("function* anonymous") {
538                sample = &sample[19..];
539                // Step 3.4: Otherwise if sample starts with "async function* anonymous", strip that from sample.
540            } else if sample.starts_with("async function* anonymous") {
541                sample = &sample[25..];
542            }
543        }
544        // Step 4: For each policy in global’s CSP list:
545        for policy in &self.0 {
546            // Step 4.1: If policy’s directive set does not contain a directive whose name is "require-trusted-types-for", skip to the next policy.
547            let directive = policy
548                .directive_set
549                .iter()
550                .find(|directive| directive.name == "require-trusted-types-for");
551            // Step 4.2: Let directive be the policy’s directive set’s directive whose name is "require-trusted-types-for"
552            let Some(directive) = directive else { continue };
553            // Step 4.3: If directive’s value does not contain a trusted-types-sink-group which is a match for sinkGroup, skip to the next policy.
554            if !directive.value.contains(sink_group) {
555                continue;
556            }
557            // Step 4.6: Let trimmedSample be the substring of sample, containing its first 40 characters.
558            let mut trimmed_sample: String = sample.into();
559            trimmed_sample.truncate(40);
560            // Step 4.4: Let violation be the result of executing Create a violation object for global, policy,
561            // and directive on global, policy and "require-trusted-types-for"
562            violations.push(Violation {
563                // Step 4.5: Set violation’s resource to "trusted-types-sink".
564                resource: ViolationResource::TrustedTypeSink {
565                    // Step 4.7: Set violation’s sample to be the result of concatenating the list « sink, trimmedSample « using "|" as a separator.
566                    sample: sink.to_owned() + "|" + &trimmed_sample,
567                },
568                directive: directive.clone(),
569                policy: policy.clone(),
570            });
571            // Step 4.9: If policy’s disposition is "enforce", then set result to "Blocked".
572            if policy.disposition == PolicyDisposition::Enforce {
573                result = Blocked
574            }
575        }
576        // Step 2: Return false.
577        (result, violations)
578    }
579    /// <https://html.spec.whatwg.org/multipage/#csp-derived-sandboxing-flags>
580    pub fn get_sandboxing_flag_set_for_document(&self) -> Option<SandboxingFlagSet> {
581        // Step 1. Let directives be an empty ordered set.
582        // Step 2. For each policy in cspList:
583        self.0
584            .iter()
585            .flat_map(|policy| {
586                policy
587                    .directive_set
588                    .iter()
589                    // Step 4. Let directive be directives[directives's size − 1].
590                    .rev()
591                    // Step 2.2. If policy's directive set contains a directive whose name is "sandbox",
592                    // then append that directive to directives.
593                    .find(|directive| directive.name == "sandbox")
594                    .and_then(|directive| directive.get_sandboxing_flag_set_for_document(policy))
595            })
596            // Step 3. If directives is empty, then return an empty sandboxing flag set.
597            .next()
598    }
599    /// https://www.w3.org/TR/CSP/#can-compile-strings
600    pub fn is_js_evaluation_allowed(&self, source: &str) -> (CheckResult, Vec<Violation>) {
601        let mut result = CheckResult::Allowed;
602        let mut violations = Vec::new();
603        // Step 5: For each policy of global’s CSP list:
604        for policy in &self.0 {
605            // Step 5.1: Let source-list be null.
606            let directive = policy
607                .directive_set
608                .iter()
609                // Step 5.2: If policy contains a directive whose name is "script-src",
610                // then set source-list to that directive’s value.
611                .find(|directive| directive.name == "script-src")
612                // Step 5.2: Otherwise if policy contains a directive whose name is "default-src",
613                // then set source-list to that directive’s value.
614                .or_else(|| {
615                    policy
616                        .directive_set
617                        .iter()
618                        .find(|directive| directive.name == "default-src")
619                });
620            // Step 5.3: If source-list is not null:
621            let Some(directive) = directive else { continue };
622            let source_list = SourceList(&directive.value);
623            if source_list.does_a_source_list_allow_js_evaluation() == AllowResult::Allows {
624                continue;
625            }
626            // Step 5.3.1: Let trustedTypesRequired be the result of executing
627            // Does sink type require trusted types?, with realm, 'script', and false.
628            let trusted_types_required =
629                self.does_sink_type_require_trusted_types("'script'", false);
630            // Step 5.3.2: If trustedTypesRequired is true and source-list contains a source expression
631            // which is an ASCII case-insensitive match for the string "'trusted-types-eval'", then skip the following steps.
632            if trusted_types_required
633                && directive
634                    .value
635                    .iter()
636                    .any(|t| ascii_case_insensitive_match(&t[..], "'trusted-types-eval'"))
637            {
638                continue;
639            }
640            // Step 5.3.3: If source-list contains a source expression which is
641            // an ASCII case-insensitive match for the string "'unsafe-eval'", then skip the following steps.
642            if directive
643                .value
644                .iter()
645                .any(|t| ascii_case_insensitive_match(&t[..], "'unsafe-eval'"))
646            {
647                continue;
648            }
649            // Step 5.3.6: If source-list contains the expression "'report-sample'",
650            // then set violation’s sample to the substring of sourceString containing its first 40 characters.
651            let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
652                let max_length = cmp::min(40, source.len());
653                Some(source[0..max_length].to_owned())
654            } else {
655                None
656            };
657            // Step 5.3.4: Let violation be the result of executing Create a violation object for global, policy,
658            // and directive on global, policy and "require-trusted-types-for"
659            violations.push(Violation {
660                // Step 5.3.5: Set violation’s resource to "eval".
661                resource: ViolationResource::Eval { sample },
662                directive: directive.clone(),
663                policy: policy.clone(),
664            });
665            // Step 5.3.8: If policy’s disposition is "enforce", then set result to "Blocked".
666            if policy.disposition == PolicyDisposition::Enforce {
667                result = CheckResult::Blocked
668            }
669        }
670        (result, violations)
671    }
672    /// https://www.w3.org/TR/CSP/#can-compile-wasm-bytes
673    pub fn is_wasm_evaluation_allowed(&self) -> (CheckResult, Vec<Violation>) {
674        let mut result = CheckResult::Allowed;
675        let mut violations = Vec::new();
676        // Step 3: For each policy of global’s CSP list:
677        for policy in &self.0 {
678            // Step 3.1: Let source-list be null.
679            let directive = policy
680                .directive_set
681                .iter()
682                // Step 3.2: If policy contains a directive whose name is "script-src",
683                // then set source-list to that directive’s value.
684                .find(|directive| directive.name == "script-src")
685                // Step 3.2: Otherwise if policy contains a directive whose name is "default-src",
686                // then set source-list to that directive’s value.
687                .or_else(|| {
688                    policy
689                        .directive_set
690                        .iter()
691                        .find(|directive| directive.name == "default-src")
692                });
693            let Some(directive) = directive else { continue };
694            let source_list = SourceList(&directive.value);
695            // Step 3.3: If source-list is non-null, and does not contain a source expression
696            // which is an ASCII case-insensitive match for the string "'unsafe-eval'",
697            // and does not contain a source expression which is an ASCII case-insensitive
698            // match for the string "'wasm-unsafe-eval'", then:
699            if source_list.does_a_source_list_allow_wasm_evaluation() == AllowResult::Allows {
700                continue;
701            }
702            // Step 3.3.1: Let violation be the result of executing § 2.4.1 Create a violation
703            // object for global, policy, and directive on global, policy, and "script-src".
704            violations.push(Violation {
705                // Step 5.3.5: Set violation’s resource to "wasm-eval".
706                resource: ViolationResource::WasmEval,
707                directive: directive.clone(),
708                policy: policy.clone(),
709            });
710            // Step 3.3.4: If policy’s disposition is "enforce", then set result to "Blocked".
711            if policy.disposition == PolicyDisposition::Enforce {
712                result = CheckResult::Blocked
713            }
714        }
715        (result, violations)
716    }
717    /// <https://w3c.github.io/webappsec-csp/#should-block-navigation-request>
718    ///
719    /// Here, `url_processor` is a callback to process trusted types (if applicable).
720    /// In case the Trusted Types algorithm returns an Error, return a None. Otherwise
721    /// return a Some with the string as provided by the policy.
722    ///
723    /// If trusted types are not applicable, then the `url_processor` can look like this:
724    /// ```rust
725    /// |s: &str| Some(s.to_owned());
726    /// ```
727    pub fn should_navigation_request_be_blocked<TrustedTypesUrlProcessor>(
728        &self,
729        request: &mut Request,
730        navigation_check_type: NavigationCheckType,
731        mut url_processor: TrustedTypesUrlProcessor,
732    ) -> (CheckResult, Vec<Violation>)
733    where
734        TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>,
735    {
736        // Step 1: Let result be "Allowed".
737        let mut result = CheckResult::Allowed;
738        let mut violations = Vec::new();
739        // Step 2: For each policy of navigation request’s policy container’s CSP list:
740        for policy in &self.0 {
741            // Step 2.1: For each directive of policy:
742            for directive in &policy.directive_set {
743                // Step 2.1.1: If directive’s pre-navigation check returns "Allowed"
744                // when executed upon navigation request, type, and policy skip to the next directive.
745                if directive.pre_navigation_check(
746                    request,
747                    navigation_check_type,
748                    &mut url_processor,
749                    policy,
750                ) == CheckResult::Allowed
751                {
752                    continue;
753                }
754                // Step 2.1.2: Otherwise, let violation be the result of executing
755                // § 2.4.1 Create a violation object for global, policy, and directive
756                // on navigation request’s client’s global object, policy, and directive’s name.
757                violations.push(Violation {
758                    // Step 2.1.3: Set violation’s resource to navigation request’s URL.
759                    resource: ViolationResource::Url(request.url.clone()),
760                    directive: Directive {
761                        name: get_the_effective_directive_for_request(request).to_owned(),
762                        value: directive.value.clone(),
763                    },
764                    policy: policy.clone(),
765                });
766                // Step 2.1.5: If policy’s disposition is "enforce", then set result to "Blocked".
767                if policy.disposition == PolicyDisposition::Enforce {
768                    result = CheckResult::Blocked;
769                }
770            }
771        }
772        // Step 3: If result is "Allowed", and if navigation request’s current URL’s scheme is javascript:
773        if result == CheckResult::Allowed && request.current_url.scheme() == "javascript" {
774            // Step 3.1: For each policy of navigation request’s policy container’s CSP list:
775            for policy in &self.0 {
776                // Step 3.1.1: For each directive of policy:
777                for directive in &policy.directive_set {
778                    // Step 3.1.1.2: If directive’s inline check returns "Allowed" when executed upon null,
779                    // "navigation" and navigation request’s current URL, skip to the next directive.
780                    if directive.inline_check(
781                        &Element { nonce: None },
782                        InlineCheckType::Navigation,
783                        policy,
784                        request.current_url.as_str(),
785                    ) == CheckResult::Allowed
786                    {
787                        continue;
788                    }
789                    // Step 3.1.1.3: Otherwise, let violation be the result of executing
790                    // § 2.4.1 Create a violation object for global, policy, and directive
791                    // on navigation request’s client’s global object, policy, and directive’s name.
792                    violations.push(Violation {
793                        // Step 3.1.1.4: Set violation’s resource to navigation request’s URL.
794                        resource: ViolationResource::Inline { sample: None },
795                        directive: Directive {
796                            // Step 3.1.1.1: Let directive-name be the result of executing
797                            // § 6.8.2 Get the effective directive for inline checks on type.
798                            name: get_the_effective_directive_for_inline_checks(
799                                InlineCheckType::Navigation,
800                            )
801                            .to_owned(),
802                            value: directive.value.clone(),
803                        },
804                        policy: policy.clone(),
805                    });
806                    // Step 3.1.1.6: If policy’s disposition is "enforce", then set result to "Blocked".
807                    if policy.disposition == PolicyDisposition::Enforce {
808                        result = CheckResult::Blocked;
809                    }
810                }
811            }
812        }
813        (result, violations)
814    }
815    /// <https://w3c.github.io/webappsec-csp/#should-block-navigation-response>
816    pub fn should_navigation_response_to_navigation_request_be_blocked(
817        &self,
818        response: &Response,
819        self_origin: &Origin,
820        parent_navigable_origins: &Vec<Url>,
821    ) -> (CheckResult, Vec<Violation>) {
822        // Step 1. Let result be "Allowed".
823        let mut result = CheckResult::Allowed;
824        let mut violations = Vec::new();
825        // Step 2. For each policy of response CSP list’s policies:
826        for policy in &self.0 {
827            // Step 2.1. For each directive of policy:
828            for directive in &policy.directive_set {
829                // Step 2.1.1. If directive’s navigation response check returns "Allowed"
830                // when executed upon navigation request, type, navigation response, target,
831                // "response", policy, and response CSP list’s self-origin, skip to the next directive.
832                if directive.navigation_response_check(
833                    response,
834                    self_origin,
835                    parent_navigable_origins,
836                    policy,
837                ) == CheckResult::Allowed
838                {
839                    continue;
840                }
841                // Step 2.1.2. Otherwise, let violation be the result of executing
842                // § 2.4.1 Create a violation object for global, policy, and directive on null, policy, and directive’s name.
843                violations.push(Violation {
844                    // Step 2.1.3. Set violation’s resource to navigation response’s URL.
845                    resource: ViolationResource::Url(response.url.clone()),
846                    directive: directive.clone(),
847                    policy: policy.clone(),
848                });
849                // Step 2.1.5. If policy’s disposition is "enforce", then set result to "Blocked".
850                if policy.disposition == PolicyDisposition::Enforce {
851                    result = CheckResult::Blocked;
852                }
853            }
854        }
855        // Step 3. For each policy of navigation request’s policy container’s CSP list’s policies:
856        //
857        // Note: We do not implement this step, since there is no directive yet that requires it
858        (result, violations)
859    }
860}
861
862#[derive(Clone, Debug)]
863pub struct Element<'a> {
864    /// When there is no nonce, populate this member with `None`.
865    ///
866    /// When the element is not [nonceable], also populate it with `None`.
867    ///
868    /// [nonceable]: https://www.w3.org/TR/CSP/#is-element-nonceable
869    pub nonce: Option<Cow<'a, str>>,
870}
871
872/**
873The valid values for type are "script", "script attribute", "style", and "style attribute".
874
875https://www.w3.org/TR/CSP/#should-block-inline
876*/
877#[derive(Clone, Copy, Debug, Eq, PartialEq)]
878pub enum InlineCheckType {
879    Script,
880    ScriptAttribute,
881    Style,
882    StyleAttribute,
883    Navigation,
884}
885
886/**
887The valid values for type are "form-submission" and "other".
888
889https://w3c.github.io/webappsec-csp/#directive-pre-navigation-check
890*/
891#[derive(Clone, Copy, Debug, Eq, PartialEq)]
892pub enum NavigationCheckType {
893    FormSubmission,
894    Other,
895}
896
897/**
898request to be validated
899
900https://fetch.spec.whatwg.org/#concept-request
901*/
902#[derive(Clone, Debug)]
903pub struct Request {
904    pub url: Url,
905    pub current_url: Url,
906    pub origin: Origin,
907    pub redirect_count: u32,
908    pub destination: Destination,
909    pub initiator: Initiator,
910    pub nonce: String,
911    pub integrity_metadata: String,
912    pub parser_metadata: ParserMetadata,
913}
914
915#[derive(Clone, Copy, Debug, Eq, PartialEq)]
916pub enum ParserMetadata {
917    ParserInserted,
918    NotParserInserted,
919    None,
920}
921
922#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
923#[derive(Clone, Copy, Debug, Eq, PartialEq)]
924pub enum Initiator {
925    Download,
926    ImageSet,
927    Manifest,
928    Prefetch,
929    Prerender,
930    Fetch,
931    Xslt,
932    None,
933}
934
935#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
936#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
937pub enum Destination {
938    None,
939    Audio,
940    AudioWorklet,
941    Document,
942    Embed,
943    Font,
944    Frame,
945    IFrame,
946    Image,
947    Json,
948    Manifest,
949    Object,
950    PaintWorklet,
951    Report,
952    Script,
953    ServiceWorker,
954    SharedWorker,
955    Style,
956    Track,
957    Video,
958    WebIdentity,
959    Worker,
960    Xslt,
961}
962
963pub struct InvalidDestination;
964
965impl FromStr for Destination {
966    type Err = InvalidDestination;
967
968    fn from_str(s: &str) -> Result<Self, Self::Err> {
969        let destination = match s {
970            "" => Self::None,
971            "audio" => Self::Audio,
972            "audioworklet" => Self::AudioWorklet,
973            "document" => Self::Document,
974            "embed" => Self::Embed,
975            "font" => Self::Font,
976            "frame" => Self::Frame,
977            "iframe" => Self::IFrame,
978            "image" => Self::Image,
979            "json" => Self::Json,
980            "manifest" => Self::Manifest,
981            "object" => Self::Object,
982            "paintworklet" => Self::PaintWorklet,
983            "report" => Self::Report,
984            "script" => Self::Script,
985            "serviceworker" => Self::ServiceWorker,
986            "sharedworker" => Self::SharedWorker,
987            "style" => Self::Style,
988            "track" => Self::Track,
989            "video" => Self::Video,
990            "webidentity" => Self::WebIdentity,
991            "worker" => Self::Worker,
992            "xslt" => Self::Xslt,
993            _ => return Err(InvalidDestination),
994        };
995
996        Ok(destination)
997    }
998}
999
1000impl Destination {
1001    /// https://fetch.spec.whatwg.org/#request-destination-script-like
1002    pub fn is_script_like(self) -> bool {
1003        use Destination::*;
1004        matches!(
1005            self,
1006            AudioWorklet | PaintWorklet | Script | ServiceWorker | SharedWorker | Worker | Xslt
1007        )
1008    }
1009
1010    pub const fn as_str(&self) -> &'static str {
1011        match self {
1012            Self::None => "",
1013            Self::Audio => "audio",
1014            Self::AudioWorklet => "audioworklet",
1015            Self::Document => "document",
1016            Self::Embed => "embed",
1017            Self::Font => "font",
1018            Self::Frame => "frame",
1019            Self::IFrame => "iframe",
1020            Self::Image => "image",
1021            Self::Json => "json",
1022            Self::Manifest => "manifest",
1023            Self::Object => "object",
1024            Self::PaintWorklet => "paintworklet",
1025            Self::Report => "report",
1026            Self::Script => "script",
1027            Self::ServiceWorker => "serviceworker",
1028            Self::SharedWorker => "sharedworker",
1029            Self::Style => "style",
1030            Self::Track => "track",
1031            Self::Video => "video",
1032            Self::WebIdentity => "webidentity",
1033            Self::Worker => "worker",
1034            Self::Xslt => "xslt",
1035        }
1036    }
1037}
1038
1039/**
1040response to be validated
1041https://fetch.spec.whatwg.org/#concept-response
1042*/
1043#[derive(Clone, Debug)]
1044pub struct Response {
1045    pub url: Url,
1046    pub redirect_count: u32,
1047}
1048
1049/// <https://fetch.spec.whatwg.org/#is-local>
1050fn is_local_url(url: &Url) -> bool {
1051    // > A URL is local if its scheme is a local scheme.
1052    let scheme = url.scheme();
1053    // > A local scheme is "about", "blob", or "data".
1054    scheme == "about" || scheme == "blob" || scheme == "data"
1055}
1056
1057/**
1058violation information
1059
1060https://www.w3.org/TR/CSP/#violation
1061*/
1062#[derive(Clone, Debug)]
1063#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1064pub struct Violation {
1065    pub resource: ViolationResource,
1066    pub directive: Directive,
1067    pub policy: Policy,
1068}
1069
1070/**
1071violation information
1072
1073https://www.w3.org/TR/CSP/#violation
1074*/
1075#[derive(Clone, Debug, Eq, PartialEq)]
1076#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1077pub enum ViolationResource {
1078    Url(Url),
1079    Inline { sample: Option<String> },
1080    TrustedTypePolicy { sample: String },
1081    TrustedTypeSink { sample: String },
1082    Eval { sample: Option<String> },
1083    WasmEval,
1084}
1085
1086/**
1087Many algorithms are allowed to return either "Allowed" or "Blocked".
1088The spec describes these as strings.
1089*/
1090#[derive(Clone, Debug, Eq, PartialEq)]
1091#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1092pub enum CheckResult {
1093    Allowed,
1094    Blocked,
1095}
1096
1097/**
1098https://www.w3.org/TR/CSP/#does-request-violate-policy
1099*/
1100#[derive(Clone, Debug, Eq, PartialEq)]
1101#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1102pub enum Violates {
1103    DoesNotViolate,
1104    Directive(Directive),
1105}
1106
1107/// https://www.w3.org/TR/CSP/#policy-disposition
1108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1109#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1110pub enum PolicyDisposition {
1111    Enforce,
1112    Report,
1113}
1114
1115/// https://www.w3.org/TR/CSP/#policy-source
1116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1117#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1118pub enum PolicySource {
1119    Header,
1120    Meta,
1121}
1122
1123/// https://www.w3.org/TR/CSP/#directives
1124#[derive(Clone, Debug, Eq, PartialEq)]
1125#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1126pub struct Directive {
1127    pub name: String,
1128    pub value: Vec<String>,
1129}
1130
1131impl Display for Directive {
1132    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
1133        <str as Display>::fmt(&self.name[..], f)?;
1134        write!(f, " ")?;
1135        for (i, token) in self.value.iter().enumerate() {
1136            if i != 0 {
1137                write!(f, " ")?;
1138            }
1139            <str as Display>::fmt(&token[..], f)?;
1140        }
1141        Ok(())
1142    }
1143}
1144
1145impl Directive {
1146    /// https://www.w3.org/TR/CSP/#serialized-directive
1147    pub fn is_valid(&self) -> bool {
1148        DIRECTIVE_NAME_GRAMMAR.is_match(&self.name)
1149            && self
1150                .value
1151                .iter()
1152                .all(|t| DIRECTIVE_VALUE_TOKEN_GRAMMAR.is_match(&t[..]))
1153    }
1154    /// https://www.w3.org/TR/CSP/#directive-pre-request-check
1155    pub fn pre_request_check(&self, request: &Request, policy: &Policy) -> CheckResult {
1156        use CheckResult::*;
1157        match &self.name[..] {
1158            "child-src" => {
1159                let name = get_the_effective_directive_for_request(request);
1160                if !should_fetch_directive_execute(name, "child-src", policy) {
1161                    return Allowed;
1162                }
1163                (Directive {
1164                    name: String::from(name),
1165                    value: self.value.clone(),
1166                })
1167                .pre_request_check(request, policy)
1168            }
1169            "connect-src" => {
1170                let name = get_the_effective_directive_for_request(request);
1171                if !should_fetch_directive_execute(name, "connect-src", policy) {
1172                    return Allowed;
1173                }
1174                if SourceList(&self.value[..]).does_request_match_source_list(request)
1175                    == DoesNotMatch
1176                {
1177                    return Blocked;
1178                }
1179                Allowed
1180            }
1181            "default-src" => {
1182                let name = get_the_effective_directive_for_request(request);
1183                if !should_fetch_directive_execute(name, "default-src", policy) {
1184                    return Allowed;
1185                }
1186                (Directive {
1187                    name: String::from(name),
1188                    value: self.value.clone(),
1189                })
1190                .pre_request_check(request, policy)
1191            }
1192            "font-src" => {
1193                let name = get_the_effective_directive_for_request(request);
1194                if !should_fetch_directive_execute(name, "font-src", policy) {
1195                    return Allowed;
1196                }
1197                if SourceList(&self.value[..]).does_request_match_source_list(request)
1198                    == DoesNotMatch
1199                {
1200                    return Blocked;
1201                }
1202                Allowed
1203            }
1204            "frame-src" => {
1205                let name = get_the_effective_directive_for_request(request);
1206                if !should_fetch_directive_execute(name, "frame-src", policy) {
1207                    return Allowed;
1208                }
1209                if SourceList(&self.value[..]).does_request_match_source_list(request)
1210                    == DoesNotMatch
1211                {
1212                    return Blocked;
1213                }
1214                Allowed
1215            }
1216            "img-src" => {
1217                let name = get_the_effective_directive_for_request(request);
1218                if !should_fetch_directive_execute(name, "img-src", policy) {
1219                    return Allowed;
1220                }
1221                if SourceList(&self.value[..]).does_request_match_source_list(request)
1222                    == DoesNotMatch
1223                {
1224                    return Blocked;
1225                }
1226                Allowed
1227            }
1228            "manifest-src" => {
1229                let name = get_the_effective_directive_for_request(request);
1230                if !should_fetch_directive_execute(name, "manifest-src", policy) {
1231                    return Allowed;
1232                }
1233                if SourceList(&self.value[..]).does_request_match_source_list(request)
1234                    == DoesNotMatch
1235                {
1236                    return Blocked;
1237                }
1238                Allowed
1239            }
1240            "media-src" => {
1241                let name = get_the_effective_directive_for_request(request);
1242                if !should_fetch_directive_execute(name, "media-src", policy) {
1243                    return Allowed;
1244                }
1245                if SourceList(&self.value[..]).does_request_match_source_list(request)
1246                    == DoesNotMatch
1247                {
1248                    return Blocked;
1249                }
1250                Allowed
1251            }
1252            "object-src" => {
1253                let name = get_the_effective_directive_for_request(request);
1254                if !should_fetch_directive_execute(name, "object-src", policy) {
1255                    return Allowed;
1256                }
1257                if SourceList(&self.value[..]).does_request_match_source_list(request)
1258                    == DoesNotMatch
1259                {
1260                    return Blocked;
1261                }
1262                Allowed
1263            }
1264            "script-src" => {
1265                let name = get_the_effective_directive_for_request(request);
1266                if !should_fetch_directive_execute(name, "script-src", policy) {
1267                    return Allowed;
1268                }
1269                script_directives_prerequest_check(request, self)
1270            }
1271            "script-src-elem" => {
1272                let name = get_the_effective_directive_for_request(request);
1273                if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1274                    return Allowed;
1275                }
1276                script_directives_prerequest_check(request, self)
1277            }
1278            "style-src" => {
1279                let name = get_the_effective_directive_for_request(request);
1280                if !should_fetch_directive_execute(name, "style-src", policy) {
1281                    return Allowed;
1282                }
1283                let source_list = SourceList(&self.value);
1284                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1285                    return Allowed;
1286                }
1287                if source_list.does_request_match_source_list(request) == DoesNotMatch {
1288                    return Blocked;
1289                }
1290                Allowed
1291            }
1292            "style-src-elem" => {
1293                let name = get_the_effective_directive_for_request(request);
1294                if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1295                    return Allowed;
1296                }
1297                let source_list = SourceList(&self.value);
1298                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1299                    return Allowed;
1300                }
1301                if source_list.does_request_match_source_list(request) == DoesNotMatch {
1302                    return Blocked;
1303                }
1304                Allowed
1305            }
1306            "worker-src" => {
1307                let name = get_the_effective_directive_for_request(request);
1308                if !should_fetch_directive_execute(name, "worker-src", policy) {
1309                    return Allowed;
1310                }
1311                let source_list = SourceList(&self.value);
1312                if source_list.does_request_match_source_list(request) == DoesNotMatch {
1313                    return Blocked;
1314                }
1315                Allowed
1316            }
1317            _ => Allowed,
1318        }
1319    }
1320    /// https://www.w3.org/TR/CSP/#directive-post-request-check
1321    pub fn post_request_check(
1322        &self,
1323        request: &Request,
1324        response: &Response,
1325        policy: &Policy,
1326    ) -> CheckResult {
1327        use CheckResult::*;
1328        match &self.name[..] {
1329            "child-src" => {
1330                let name = get_the_effective_directive_for_request(request);
1331                if !should_fetch_directive_execute(name, "child-src", policy) {
1332                    return Allowed;
1333                }
1334                Directive {
1335                    name: name.to_owned(),
1336                    value: self.value.clone(),
1337                }
1338                .post_request_check(request, response, policy)
1339            }
1340            "connect-src" => {
1341                let name = get_the_effective_directive_for_request(request);
1342                if !should_fetch_directive_execute(name, "connect-src", policy) {
1343                    return Allowed;
1344                }
1345                let source_list = SourceList(&self.value);
1346                if source_list.does_response_to_request_match_source_list(request, response)
1347                    == DoesNotMatch
1348                {
1349                    return Blocked;
1350                }
1351                Allowed
1352            }
1353            "default-src" => {
1354                let name = get_the_effective_directive_for_request(request);
1355                if !should_fetch_directive_execute(name, "default-src", policy) {
1356                    return Allowed;
1357                }
1358                Directive {
1359                    name: name.to_owned(),
1360                    value: self.value.clone(),
1361                }
1362                .post_request_check(request, response, policy)
1363            }
1364            "font-src" => {
1365                let name = get_the_effective_directive_for_request(request);
1366                if !should_fetch_directive_execute(name, "font-src", policy) {
1367                    return Allowed;
1368                }
1369                let source_list = SourceList(&self.value);
1370                if source_list.does_response_to_request_match_source_list(request, response)
1371                    == DoesNotMatch
1372                {
1373                    return Blocked;
1374                }
1375                Allowed
1376            }
1377            "frame-src" => {
1378                let name = get_the_effective_directive_for_request(request);
1379                if !should_fetch_directive_execute(name, "frame-src", policy) {
1380                    return Allowed;
1381                }
1382                let source_list = SourceList(&self.value);
1383                if source_list.does_response_to_request_match_source_list(request, response)
1384                    == DoesNotMatch
1385                {
1386                    return Blocked;
1387                }
1388                Allowed
1389            }
1390            "img-src" => {
1391                let name = get_the_effective_directive_for_request(request);
1392                if !should_fetch_directive_execute(name, "img-src", policy) {
1393                    return Allowed;
1394                }
1395                let source_list = SourceList(&self.value);
1396                if source_list.does_response_to_request_match_source_list(request, response)
1397                    == DoesNotMatch
1398                {
1399                    return Blocked;
1400                }
1401                Allowed
1402            }
1403            "manifest-src" => {
1404                let name = get_the_effective_directive_for_request(request);
1405                if !should_fetch_directive_execute(name, "manifest-src", policy) {
1406                    return Allowed;
1407                }
1408                let source_list = SourceList(&self.value);
1409                if source_list.does_response_to_request_match_source_list(request, response)
1410                    == DoesNotMatch
1411                {
1412                    return Blocked;
1413                }
1414                Allowed
1415            }
1416            "media-src" => {
1417                let name = get_the_effective_directive_for_request(request);
1418                if !should_fetch_directive_execute(name, "media-src", policy) {
1419                    return Allowed;
1420                }
1421                let source_list = SourceList(&self.value);
1422                if source_list.does_response_to_request_match_source_list(request, response)
1423                    == DoesNotMatch
1424                {
1425                    return Blocked;
1426                }
1427                Allowed
1428            }
1429            "object-src" => {
1430                let name = get_the_effective_directive_for_request(request);
1431                if !should_fetch_directive_execute(name, "object-src", policy) {
1432                    return Allowed;
1433                }
1434                let source_list = SourceList(&self.value);
1435                if source_list.does_response_to_request_match_source_list(request, response)
1436                    == DoesNotMatch
1437                {
1438                    return Blocked;
1439                }
1440                Allowed
1441            }
1442            "script-src" => {
1443                let name = get_the_effective_directive_for_request(request);
1444                if !should_fetch_directive_execute(name, "script-src", policy) {
1445                    return Allowed;
1446                }
1447                script_directives_postrequest_check(request, response, self)
1448            }
1449            "script-src-elem" => {
1450                let name = get_the_effective_directive_for_request(request);
1451                if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1452                    return Allowed;
1453                }
1454                script_directives_postrequest_check(request, response, self)
1455            }
1456            "style-src" => {
1457                let name = get_the_effective_directive_for_request(request);
1458                if !should_fetch_directive_execute(name, "style-src", policy) {
1459                    return Allowed;
1460                }
1461                let source_list = SourceList(&self.value);
1462                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1463                    return Allowed;
1464                }
1465                if source_list.does_response_to_request_match_source_list(request, response)
1466                    == DoesNotMatch
1467                {
1468                    return Blocked;
1469                }
1470                Allowed
1471            }
1472            "style-src-elem" => {
1473                let name = get_the_effective_directive_for_request(request);
1474                if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1475                    return Allowed;
1476                }
1477                let source_list = SourceList(&self.value);
1478                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1479                    return Allowed;
1480                }
1481                if source_list.does_response_to_request_match_source_list(request, response)
1482                    == DoesNotMatch
1483                {
1484                    return Blocked;
1485                }
1486                Allowed
1487            }
1488            "worker-src" => {
1489                let name = get_the_effective_directive_for_request(request);
1490                if !should_fetch_directive_execute(name, "worker-src", policy) {
1491                    return Allowed;
1492                }
1493                let source_list = SourceList(&self.value);
1494                if source_list.does_response_to_request_match_source_list(request, response)
1495                    == DoesNotMatch
1496                {
1497                    return Blocked;
1498                }
1499                Allowed
1500            }
1501            _ => Allowed,
1502        }
1503    }
1504    /// https://www.w3.org/TR/CSP/#directive-inline-check
1505    pub fn inline_check(
1506        &self,
1507        element: &Element,
1508        type_: InlineCheckType,
1509        policy: &Policy,
1510        source: &str,
1511    ) -> CheckResult {
1512        use CheckResult::*;
1513        match &self.name[..] {
1514            "default-src" => {
1515                let name = get_the_effective_directive_for_inline_checks(type_);
1516                if !should_fetch_directive_execute(name, "default-src", policy) {
1517                    return Allowed;
1518                }
1519                Directive {
1520                    name: name.to_owned(),
1521                    value: self.value.clone(),
1522                }
1523                .inline_check(element, type_, policy, source)
1524            }
1525            "script-src" => {
1526                let name = get_the_effective_directive_for_inline_checks(type_);
1527                if !should_fetch_directive_execute(name, "script-src", policy) {
1528                    return Allowed;
1529                }
1530                let source_list = SourceList(&self.value);
1531                if source_list
1532                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1533                    == DoesNotMatch
1534                {
1535                    return Blocked;
1536                }
1537                Allowed
1538            }
1539            "script-src-elem" => {
1540                let name = get_the_effective_directive_for_inline_checks(type_);
1541                if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1542                    return Allowed;
1543                }
1544                let source_list = SourceList(&self.value);
1545                if source_list
1546                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1547                    == DoesNotMatch
1548                {
1549                    return Blocked;
1550                }
1551                Allowed
1552            }
1553            "script-src-attr" => {
1554                let name = get_the_effective_directive_for_inline_checks(type_);
1555                if !should_fetch_directive_execute(name, "script-src-attr", policy) {
1556                    return Allowed;
1557                }
1558                let source_list = SourceList(&self.value);
1559                if source_list
1560                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1561                    == DoesNotMatch
1562                {
1563                    return Blocked;
1564                }
1565                Allowed
1566            }
1567            "style-src" => {
1568                let name = get_the_effective_directive_for_inline_checks(type_);
1569                if !should_fetch_directive_execute(name, "style-src", policy) {
1570                    return Allowed;
1571                }
1572                let source_list = SourceList(&self.value);
1573                if source_list
1574                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1575                    == DoesNotMatch
1576                {
1577                    return Blocked;
1578                }
1579                Allowed
1580            }
1581            "style-src-elem" => {
1582                let name = get_the_effective_directive_for_inline_checks(type_);
1583                if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1584                    return Allowed;
1585                }
1586                let source_list = SourceList(&self.value);
1587                if source_list
1588                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1589                    == DoesNotMatch
1590                {
1591                    return Blocked;
1592                }
1593                Allowed
1594            }
1595            "style-src-attr" => {
1596                let name = get_the_effective_directive_for_inline_checks(type_);
1597                if !should_fetch_directive_execute(name, "style-src-attr", policy) {
1598                    return Allowed;
1599                }
1600                let source_list = SourceList(&self.value);
1601                if source_list
1602                    .does_element_match_source_list_for_type_and_source(element, type_, source)
1603                    == DoesNotMatch
1604                {
1605                    return Blocked;
1606                }
1607                Allowed
1608            }
1609            _ => Allowed,
1610        }
1611    }
1612    /// <https://html.spec.whatwg.org/multipage/#csp-derived-sandboxing-flags>
1613    pub fn get_sandboxing_flag_set_for_document(
1614        &self,
1615        policy: &Policy,
1616    ) -> Option<SandboxingFlagSet> {
1617        debug_assert!(&self.name[..] == "sandbox");
1618        // Step 2.1. If policy's disposition is not "enforce", then continue.
1619        if policy.disposition != PolicyDisposition::Enforce {
1620            None
1621        } else {
1622            // Step 5. Return the result of parsing the sandboxing directive directive.
1623            Some(parse_a_sandboxing_directive(&self.value[..]))
1624        }
1625    }
1626    /// <https://w3c.github.io/webappsec-csp/#directive-pre-navigation-check>
1627    pub fn pre_navigation_check<TrustedTypesUrlProcessor>(
1628        &self,
1629        request: &mut Request,
1630        type_: NavigationCheckType,
1631        mut url_processor: TrustedTypesUrlProcessor,
1632        _policy: &Policy,
1633    ) -> CheckResult
1634    where
1635        TrustedTypesUrlProcessor: FnMut(&str) -> Option<String>,
1636    {
1637        use CheckResult::*;
1638        match &self.name[..] {
1639            // <https://w3c.github.io/webappsec-csp/#form-action-pre-navigate>
1640            "form-action" => {
1641                // Step 2: If navigation type is "form-submission":
1642                if type_ == NavigationCheckType::FormSubmission {
1643                    let source_list = SourceList(&self.value);
1644                    // Step 2.1: If the result of executing § 6.7.2.5 Does request match source list? on request,
1645                    // this directive’s value, and a policy, is "Does Not Match", return "Blocked".
1646                    if source_list.does_request_match_source_list(request) == DoesNotMatch {
1647                        return Blocked;
1648                    }
1649                }
1650                // Step 3: Return "Allowed".
1651                Allowed
1652            }
1653            // <https://www.w3.org/TR/trusted-types/#require-trusted-types-for-pre-navigation-check>
1654            "require-trusted-types-for" => {
1655                let url = &request.url;
1656                // Step 1. If request’s url’s scheme is not "javascript", return "Allowed" and abort further steps.
1657                if url.scheme() != "javascript" {
1658                    return Allowed;
1659                }
1660                // Step 2. Let urlString be the result of running the URL serializer on request’s url.
1661                //
1662                // Already done when creating Request
1663                // Step 3. Let encodedScriptSource be the result of removing the leading "javascript:" from urlString.
1664                let encoded_script_source = &url[Position::AfterScheme..][1..];
1665                // Step 4. Let convertedScriptSource be the result of executing Process value with a default policy algorithm
1666                // If that algorithm threw an error or convertedScriptSource is not a TrustedScript object,
1667                // return "Blocked" and abort further steps.
1668                let Some(converted_script_source) = url_processor(encoded_script_source) else {
1669                    return Blocked;
1670                };
1671                // Step 5. Set urlString to be the result of prepending "javascript:" to stringified convertedScriptSource.
1672                let url_string = "javascript:".to_owned() + &converted_script_source;
1673                // Step 6. Let newURL be the result of running the URL parser on urlString.
1674                // If the parser returns a failure, return "Blocked" and abort further steps.
1675                let Ok(new_url) = Url::parse(&url_string) else {
1676                    return Blocked;
1677                };
1678                // Step 7. Set request’s url to newURL.
1679                request.url = new_url;
1680                // Step 8. Return "Allowed".
1681                Allowed
1682            }
1683            _ => Allowed,
1684        }
1685    }
1686
1687    pub fn navigation_response_check(
1688        &self,
1689        response: &Response,
1690        self_origin: &Origin,
1691        parent_navigable_origins: &Vec<Url>,
1692        _policy: &Policy,
1693    ) -> CheckResult {
1694        use CheckResult::*;
1695        match &self.name[..] {
1696            // <https://w3c.github.io/webappsec-csp/#frame-ancestors-navigation-response>
1697            "frame-ancestors" => {
1698                // Step 1. If navigation response’s URL is local, return "Allowed".
1699                if is_local_url(&response.url) {
1700                    return Allowed;
1701                }
1702                // Step 2. Assert: request, navigation response, and navigation type,
1703                // are unused from this point forward in this algorithm,
1704                // as frame-ancestors is concerned only with navigation response’s frame-ancestors directive.
1705
1706                // Step 3. If check type is "source", return "Allowed".
1707                //
1708                // We only call this once for responses
1709
1710                let source_list = SourceList(&self.value);
1711                // Step 4. If target is not a child navigable, return "Allowed".
1712                // Step 5. Let current be target.
1713                // Step 6. While current is a child navigable:
1714                for origin in parent_navigable_origins {
1715                    // Step 6.1. Let document be current’s container document.
1716                    // Step 6.2. Let origin be the result of executing the URL parser on the ASCII serialization of document’s origin.
1717                    // Step 6.3. If § 6.7.2.7 Does url match source list in origin with redirect count? returns
1718                    // Does Not Match when executed upon origin, this directive’s value, self-origin, and 0, return "Blocked".
1719                    if source_list.does_url_match_source_list_in_origin_with_redirect_count(
1720                        origin,
1721                        self_origin,
1722                        0,
1723                    ) == DoesNotMatch
1724                    {
1725                        return Blocked;
1726                    }
1727                    // Step 6.4. Set current to document’s node navigable.
1728                }
1729                // Step 7. Return "Allowed".
1730                Allowed
1731            }
1732            _ => Allowed,
1733        }
1734    }
1735}
1736
1737/// https://www.w3.org/TR/CSP/#effective-directive-for-inline-check
1738fn get_the_effective_directive_for_inline_checks(type_: InlineCheckType) -> &'static str {
1739    use InlineCheckType::*;
1740    match type_ {
1741        Script | Navigation => "script-src-elem",
1742        ScriptAttribute => "script-src-attr",
1743        Style => "style-src-elem",
1744        StyleAttribute => "style-src-attr",
1745    }
1746}
1747
1748/// <https://www.w3.org/TR/CSP/#script-pre-request>
1749fn script_directives_prerequest_check(request: &Request, directive: &Directive) -> CheckResult {
1750    use CheckResult::*;
1751    // Step 1. If request’s destination is script-like:
1752    if request_is_script_like(request) {
1753        let source_list = SourceList(&directive.value[..]);
1754        // Step 1.1. If the result of executing § 6.7.2.3 Does nonce match source list? on
1755        // request’s cryptographic nonce metadata and this directive’s value is "Matches", return "Allowed".
1756        if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1757            return Allowed;
1758        }
1759        // Step 1.2. If the result of executing § 6.7.2.4 Does integrity metadata match source list? on
1760        // request’s integrity metadata and this directive’s value is "Matches", return "Allowed".
1761        if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata)
1762            == Matches
1763        {
1764            return Allowed;
1765        }
1766        // Step 1.3. If directive’s value contains a source expression that is an
1767        // ASCII case-insensitive match for the "'strict-dynamic'" keyword-source:
1768        if directive
1769            .value
1770            .iter()
1771            .any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'"))
1772        {
1773            // Step 1.3.1. If the request’s parser metadata is "parser-inserted", return "Blocked".
1774            if request.parser_metadata == ParserMetadata::ParserInserted {
1775                return Blocked;
1776            }
1777            // Otherwise, return "Allowed".
1778            return Allowed;
1779        }
1780
1781        // Step 1.4. If the result of executing § 6.7.2.5 Does request match source list? on
1782        // request, directive’s value, and policy, is "Does Not Match", return "Blocked".
1783        if source_list.does_request_match_source_list(request) == DoesNotMatch {
1784            return Blocked;
1785        }
1786    }
1787    // Step 2. Return "Allowed".
1788    Allowed
1789}
1790
1791/// https://www.w3.org/TR/CSP/#script-post-request
1792fn script_directives_postrequest_check(
1793    request: &Request,
1794    response: &Response,
1795    directive: &Directive,
1796) -> CheckResult {
1797    use CheckResult::*;
1798    // Step 1. If request’s destination is script-like:
1799    if request_is_script_like(request) {
1800        // Step 1.1. Call potentially report hash with response, request, directive and policy.
1801        // TODO
1802        let source_list = SourceList(&directive.value[..]);
1803        // Step 1.2. If the result of executing § 6.7.2.3 Does nonce match source list? on
1804        // request’s cryptographic nonce metadata and this directive’s value is "Matches", return "Allowed".
1805        if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1806            return Allowed;
1807        }
1808        // Step 1.3. If the result of executing § 6.7.2.4 Does integrity metadata match source list? on
1809        // request’s integrity metadata and this directive’s value is "Matches", return "Allowed".
1810        if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata)
1811            == Matches
1812        {
1813            return Allowed;
1814        }
1815        // Step 1.4. If directive’s value contains "'strict-dynamic'":
1816        if directive
1817            .value
1818            .iter()
1819            .any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'"))
1820        {
1821            // Step 1.4.1. If the request’s parser metadata is "parser-inserted", return "Blocked".
1822            if request.parser_metadata == ParserMetadata::ParserInserted {
1823                return Blocked;
1824            }
1825            // Otherwise, return "Allowed".
1826            return Allowed;
1827        }
1828        // Step 1.5. If the result of executing § 6.7.2.6 Does response to request match source list? on
1829        // response, request, directive’s value, and policy, is "Does Not Match", return "Blocked".
1830        if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch
1831        {
1832            return Blocked;
1833        }
1834    }
1835    // Step 2. Return "Allowed".
1836    Allowed
1837}
1838
1839/// https://fetch.spec.whatwg.org/#request-destination-script-like
1840fn request_is_script_like(request: &Request) -> bool {
1841    request.destination.is_script_like()
1842}
1843
1844/// https://www.w3.org/TR/CSP/#should-directive-execute
1845fn should_fetch_directive_execute(
1846    effective_directive_name: &str,
1847    directive_name: &str,
1848    policy: &Policy,
1849) -> bool {
1850    let directive_fallback_list = get_fetch_directive_fallback_list(effective_directive_name);
1851    for fallback_directive in directive_fallback_list {
1852        if directive_name == *fallback_directive {
1853            return true;
1854        }
1855        if policy.contains_a_directive_whose_name_is(fallback_directive) {
1856            return false;
1857        }
1858    }
1859    false
1860}
1861
1862/// https://www.w3.org/TR/CSP/#directive-fallback-list
1863fn get_fetch_directive_fallback_list(directive_name: &str) -> &'static [&'static str] {
1864    match directive_name {
1865        "script-src-elem" => &["script-src-elem", "script-src", "default-src"],
1866        "script-src-attr" => &["script-src-attr", "script-src", "default-src"],
1867        "style-src-elem" => &["style-src-elem", "style-src", "default-src"],
1868        "style-src-attr" => &["style-src-attr", "style-src", "default-src"],
1869        "worker-src" => &["worker-src", "child-src", "script-src", "default-src"],
1870        "connect-src" => &["connect-src", "default-src"],
1871        "manifest-src" => &["manifest-src", "default-src"],
1872        "object-src" => &["object-src", "default-src"],
1873        "frame-src" => &["frame-src", "child-src", "default-src"],
1874        "media-src" => &["media-src", "default-src"],
1875        "font-src" => &["font-src", "default-src"],
1876        "img-src" => &["img-src", "default-src"],
1877        _ => &[],
1878    }
1879}
1880
1881/// https://www.w3.org/TR/CSP/#effective-directive-for-a-request
1882fn get_the_effective_directive_for_request(request: &Request) -> &'static str {
1883    use Destination::*;
1884    use Initiator::*;
1885    // Step 1: If request’s initiator is "prefetch" or "prerender", return default-src.
1886    if request.initiator == Prefetch || request.initiator == Prerender {
1887        return "default-src";
1888    }
1889    // Step 2: Switch on request’s destination, and execute the associated steps:
1890    match request.destination {
1891        Destination::Manifest => "manifest-src",
1892        Object | Embed => "object-src",
1893        Frame | IFrame => "frame-src",
1894        Audio | Track | Video => "media-src",
1895        Font => "font-src",
1896        Image => "img-src",
1897        Style => "style-src-elem",
1898        Script | Destination::Xslt | AudioWorklet | PaintWorklet => "script-src-elem",
1899        ServiceWorker | SharedWorker | Worker => "worker-src",
1900        Json | WebIdentity => "connect-src",
1901        Report => "",
1902        // Step 3: Return connect-src.
1903        _ => "connect-src",
1904    }
1905}
1906
1907/// https://www.w3.org/TR/CSP/#match-element-to-source-list
1908#[derive(Clone, Debug, Eq, PartialEq)]
1909pub enum MatchResult {
1910    Matches,
1911    DoesNotMatch,
1912}
1913
1914/// https://www.w3.org/TR/CSP/#grammardef-directive-name
1915static DIRECTIVE_NAME_GRAMMAR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^[0-9a-z\-]+$"#).unwrap());
1916/// https://www.w3.org/TR/CSP/#grammardef-directive-value
1917static DIRECTIVE_VALUE_TOKEN_GRAMMAR: Lazy<Regex> =
1918    Lazy::new(|| Regex::new(r#"^[\u{21}-\u{2B}\u{2D}-\u{3A}\u{3C}-\u{7E}]+$"#).unwrap());
1919/// https://www.w3.org/TR/CSP/#grammardef-nonce-source
1920static NONCE_SOURCE_GRAMMAR: Lazy<Regex> =
1921    Lazy::new(|| Regex::new(r#"^'nonce-(?P<n>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1922static NONE_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^'none'$"#).unwrap());
1923/// https://www.w3.org/TR/CSP/#grammardef-scheme-source
1924static SCHEME_SOURCE_GRAMMAR: Lazy<Regex> =
1925    Lazy::new(|| Regex::new(r#"^(?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*):$"#).unwrap());
1926/// https://www.w3.org/TR/CSP/#grammardef-host-source
1927static HOST_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1928    Regex::new(r#"^((?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*)://)?(?P<host>\*|(\*\.)?[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*)(?P<port>:(\*|[0-9]+))?(?P<path>/([:@%!\$&'\(\)\*\+,;=0-9a-zA-Z\-\._~]+)?(/[:@%!\$&'\(\)\*\+,;=0-9a-zA-Z\-\._~]*)*)?$"#).unwrap()
1929});
1930/// https://www.w3.org/TR/CSP/#grammardef-hash-source
1931static HASH_SOURCE_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
1932    Regex::new(r#"^'(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)'$"#)
1933        .unwrap()
1934});
1935
1936/// https://www.w3.org/TR/CSP/#framework-directive-source-list
1937#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1938struct SourceList<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>>(I);
1939
1940impl<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item = &'a U>> SourceList<'a, U, I> {
1941    /// https://www.w3.org/TR/CSP/#match-nonce-to-source-list
1942    fn does_nonce_match_source_list(&self, nonce: &str) -> MatchResult {
1943        if nonce.is_empty() {
1944            return DoesNotMatch;
1945        };
1946        for expression in self.0.clone().into_iter() {
1947            if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression.borrow()) {
1948                if let Some(captured_nonce) = captures.name("n") {
1949                    if nonce == captured_nonce.as_str() {
1950                        return Matches;
1951                    }
1952                }
1953            }
1954        }
1955        DoesNotMatch
1956    }
1957    /// https://www.w3.org/TR/CSP/#match-integrity-metadata-to-source-list
1958    fn does_integrity_metadata_match_source_list(&self, integrity_metadata: &str) -> MatchResult {
1959        // Step 2: Let integrity expressions be the set of source expressions in source list that match the hash-source grammar.
1960        let integrity_expressions: Vec<HashFunction> = self
1961            .0
1962            .clone()
1963            .into_iter()
1964            .filter_map(|expression| {
1965                if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression.borrow()) {
1966                    if let (Some(algorithm), Some(value)) = (
1967                        captures
1968                            .name("algorithm")
1969                            .and_then(|a| HashAlgorithm::from_name(a.as_str())),
1970                        captures.name("value"),
1971                    ) {
1972                        return Some(HashFunction {
1973                            algorithm,
1974                            value: String::from(value.as_str()),
1975                        });
1976                    }
1977                }
1978                None
1979            })
1980            .collect();
1981        // Step 3: If integrity expressions is empty, return "Does Not Match".
1982        if integrity_expressions.is_empty() {
1983            return DoesNotMatch;
1984        }
1985        // Step 4: Let integrity sources be the result of executing the algorithm defined in SRI § 3.3.3 Parse metadata. on integrity metadata.
1986        let integrity_sources = parse_subresource_integrity_metadata(integrity_metadata);
1987        match integrity_sources {
1988            // Step 5: If integrity sources is "no metadata" or an empty set, return "Does Not Match".
1989            SubresourceIntegrityMetadata::NoMetadata => DoesNotMatch,
1990            SubresourceIntegrityMetadata::IntegritySources(integrity_sources) => {
1991                if integrity_sources.is_empty() {
1992                    return DoesNotMatch;
1993                }
1994                // Step 6: For each source of integrity sources:
1995                for source in &integrity_sources {
1996                    // Step 6.1: If integrity expressions does not contain a source expression whose hash-algorithm
1997                    // is an ASCII case-insensitive match for source’s hash-algorithm,
1998                    // and whose base64-value is identical to source’s base64-value, return "Does Not Match".
1999                    //
2000                    // Note that the case-insensitivy is already handled in HashAlgorithm::from_name and therefore
2001                    // we can do a simple equals check here for both algorithm and value.
2002                    if !integrity_expressions.iter().any(|ex| ex == source) {
2003                        return DoesNotMatch;
2004                    }
2005                }
2006                // Step 7: Return "Matches".
2007                Matches
2008            }
2009        }
2010    }
2011    /// https://www.w3.org/TR/CSP/#match-request-to-source-list
2012    fn does_request_match_source_list(&self, request: &Request) -> MatchResult {
2013        // > Given a request request, a source list source list, and an origin self-origin,
2014        // > this algorithm returns the result of executing
2015        // > § 6.7.2.7 Does url match source list in origin with redirect count?
2016        // > on request’s current url, source list, self-origin, and request’s redirect count.
2017        self.does_url_match_source_list_in_origin_with_redirect_count(
2018            &request.current_url,
2019            &request.origin,
2020            request.redirect_count,
2021        )
2022    }
2023    /// https://www.w3.org/TR/CSP/#match-url-to-source-list
2024    fn does_url_match_source_list_in_origin_with_redirect_count(
2025        &self,
2026        url: &Url,
2027        origin: &Origin,
2028        redirect_count: u32,
2029    ) -> MatchResult {
2030        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2031            if NONE_SOURCE_GRAMMAR.is_match(expression) {
2032                continue;
2033            };
2034            let result = does_url_match_expression_in_origin_with_redirect_count(
2035                url,
2036                expression,
2037                origin,
2038                redirect_count,
2039            );
2040            if result == Matches {
2041                return Matches;
2042            }
2043        }
2044        DoesNotMatch
2045    }
2046    /// https://www.w3.org/TR/CSP/#match-element-to-source-list
2047    fn does_element_match_source_list_for_type_and_source(
2048        &self,
2049        element: &Element,
2050        type_: InlineCheckType,
2051        source: &str,
2052    ) -> MatchResult {
2053        if self.does_a_source_list_allow_all_inline_behavior_for_type(type_) == AllowResult::Allows
2054        {
2055            return Matches;
2056        }
2057        if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style {
2058            if let Some(nonce) = element.nonce.as_ref() {
2059                for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2060                    if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression) {
2061                        if let Some(captured_nonce) = captures.name("n") {
2062                            if nonce == captured_nonce.as_str() {
2063                                return Matches;
2064                            }
2065                        }
2066                    }
2067                }
2068            }
2069        }
2070        let mut unsafe_hashes = false;
2071        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2072            if ascii_case_insensitive_match(expression, "'unsafe-hashes'") {
2073                unsafe_hashes = true;
2074                break;
2075            }
2076        }
2077        if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style || unsafe_hashes {
2078            for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2079                if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression) {
2080                    if let (Some(algorithm), Some(value)) = (
2081                        captures
2082                            .name("algorithm")
2083                            .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2084                        captures.name("value"),
2085                    ) {
2086                        let actual = algorithm.apply(source);
2087                        let expected = value.as_str().replace('-', "+").replace('_', "/");
2088                        if actual == expected {
2089                            return Matches;
2090                        }
2091                    }
2092                }
2093            }
2094        }
2095        DoesNotMatch
2096    }
2097    /// https://www.w3.org/TR/CSP/#allow-all-inline
2098    fn does_a_source_list_allow_all_inline_behavior_for_type(
2099        &self,
2100        type_: InlineCheckType,
2101    ) -> AllowResult {
2102        use InlineCheckType::*;
2103        let mut allow_all_inline = false;
2104        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2105            if HASH_SOURCE_GRAMMAR.is_match(expression) || NONCE_SOURCE_GRAMMAR.is_match(expression)
2106            {
2107                return AllowResult::DoesNotAllow;
2108            }
2109            if (type_ == Script || type_ == ScriptAttribute || type_ == Navigation)
2110                && expression == "'strict-dynamic'"
2111            {
2112                return AllowResult::DoesNotAllow;
2113            }
2114            if ascii_case_insensitive_match(expression, "'unsafe-inline'") {
2115                allow_all_inline = true;
2116            }
2117        }
2118        if allow_all_inline {
2119            AllowResult::Allows
2120        } else {
2121            AllowResult::DoesNotAllow
2122        }
2123    }
2124    /// https://www.w3.org/TR/CSP/#match-response-to-source-list
2125    fn does_response_to_request_match_source_list(
2126        &self,
2127        request: &Request,
2128        response: &Response,
2129    ) -> MatchResult {
2130        self.does_url_match_source_list_in_origin_with_redirect_count(
2131            &response.url,
2132            &request.origin,
2133            response.redirect_count,
2134        )
2135    }
2136    /// https://www.w3.org/TR/CSP/#can-compile-strings
2137    fn does_a_source_list_allow_js_evaluation(&self) -> AllowResult {
2138        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2139            // Step 5.3: If source-list contains a source expression which is an ASCII case-insensitive match
2140            // for the string "'unsafe-eval'", then skip the following steps.
2141            if ascii_case_insensitive_match(expression, "'unsafe-eval'") {
2142                return AllowResult::Allows;
2143            }
2144        }
2145        AllowResult::DoesNotAllow
2146    }
2147    /// https://www.w3.org/TR/CSP/#can-compile-wasm-bytes
2148    fn does_a_source_list_allow_wasm_evaluation(&self) -> AllowResult {
2149        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
2150            if ascii_case_insensitive_match(expression, "'unsafe-eval'")
2151                || ascii_case_insensitive_match(expression, "'wasm-unsafe-eval'")
2152            {
2153                return AllowResult::Allows;
2154            }
2155        }
2156        AllowResult::DoesNotAllow
2157    }
2158}
2159
2160/// https://www.w3.org/TR/CSP/#allow-all-inline
2161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2162enum AllowResult {
2163    Allows,
2164    DoesNotAllow,
2165}
2166
2167/// https://www.w3.org/TR/CSP/#match-url-to-source-expression
2168fn does_url_match_expression_in_origin_with_redirect_count(
2169    url: &Url,
2170    expression: &str,
2171    origin: &Origin,
2172    redirect_count: u32,
2173) -> MatchResult {
2174    let url_scheme = url.scheme();
2175    if expression == "*" {
2176        if scheme_is_network(url_scheme) {
2177            return Matches;
2178        }
2179        return origin_scheme_part_match(origin, url_scheme);
2180    }
2181    if let Some(captures) = SCHEME_SOURCE_GRAMMAR.captures(expression) {
2182        if let Some(expression_scheme) = captures.name("scheme") {
2183            return scheme_part_match(expression_scheme.as_str(), url_scheme);
2184        }
2185        // It should not be possible to match HOST_SOURCE_GRAMMAR without having a scheme part
2186        return DoesNotMatch;
2187    }
2188    if let Some(captures) = HOST_SOURCE_GRAMMAR.captures(expression) {
2189        let expr_has_scheme_part = if let Some(expression_scheme) = captures.name("scheme") {
2190            if scheme_part_match(expression_scheme.as_str(), url_scheme) != Matches {
2191                return DoesNotMatch;
2192            }
2193            true
2194        } else {
2195            false
2196        };
2197        let url_host = if let Some(url_host) = url.host() {
2198            url_host
2199        } else {
2200            return DoesNotMatch;
2201        };
2202        if !expr_has_scheme_part && origin_scheme_part_match(origin, url.scheme()) != Matches {
2203            return DoesNotMatch;
2204        }
2205        if let Some(expression_host) = captures.name("host") {
2206            if host_part_match(expression_host.as_str(), &url_host.to_string()) != Matches {
2207                return DoesNotMatch;
2208            }
2209        } else {
2210            // It should not be possible to match HOST_SOURCE_GRAMMAR without having a host part
2211            return DoesNotMatch;
2212        }
2213        // Skip the first byte of the port capture to avoid the `:`.
2214        let port_part = captures.name("port").map(|port| &port.as_str()[1..]);
2215        if port_part_match(port_part, url) != Matches {
2216            return DoesNotMatch;
2217        }
2218        let path_part = captures
2219            .name("path")
2220            .map(|path_part| path_part.as_str())
2221            .unwrap_or("");
2222        if path_part != "/" && redirect_count == 0 {
2223            let path = url.path();
2224            if path_part_match(path_part, path) != Matches {
2225                return DoesNotMatch;
2226            }
2227        }
2228        return Matches;
2229    }
2230    if ascii_case_insensitive_match(expression, "'self'") {
2231        if *origin == url.origin() {
2232            return Matches;
2233        }
2234        if let Origin::Tuple(scheme, host, port) = origin {
2235            let hosts_are_the_same = Some(host) == url.host().map(|p| p.to_owned()).as_ref();
2236            let ports_are_the_same = Some(*port) == url.port();
2237            let origins_port_is_default_for_scheme = Some(*port) == default_port(scheme);
2238            let url_port_is_default_port_for_scheme =
2239                url.port() == default_port(scheme) && default_port(scheme).is_some();
2240            let ports_are_default =
2241                url_port_is_default_port_for_scheme && origins_port_is_default_for_scheme;
2242            if hosts_are_the_same
2243                && (ports_are_the_same || ports_are_default)
2244                && ((url_scheme == "https" || url_scheme == "wss")
2245                    || (scheme == "http" && (url_scheme == "http" || url_scheme == "ws")))
2246            {
2247                return Matches;
2248            }
2249        }
2250    }
2251    DoesNotMatch
2252}
2253
2254/// https://www.w3.org/TR/CSP/#match-hosts
2255fn host_part_match(pattern: &str, host: &str) -> MatchResult {
2256    debug_assert!(!host.is_empty());
2257    // Step 1. If host is not a domain, return "Does Not Match".
2258    if host.is_empty() {
2259        return DoesNotMatch;
2260    }
2261    if pattern.as_bytes()[0] == b'*' {
2262        // Step 2. If pattern is "*", return "Matches".
2263        if pattern.len() == 1 {
2264            return Matches;
2265        }
2266        // Step 3. If pattern starts with "*.":
2267        if pattern.as_bytes()[1] == b'.' {
2268            // Step 3.1 Let remaining be pattern with the leading U+002A (*) removed and ASCII lowercased.
2269            let remaining_pattern = &pattern[1..];
2270            if remaining_pattern.len() > host.len() {
2271                return DoesNotMatch;
2272            }
2273            let remaining_host = &host[(host.len() - remaining_pattern.len())..];
2274            debug_assert_eq!(remaining_host.len(), remaining_pattern.len());
2275            // Step 3.2. If host to ASCII lowercase ends with remaining, then return "Matches".
2276            if ascii_case_insensitive_match(remaining_pattern, remaining_host) {
2277                return Matches;
2278            }
2279            // Step 3.3 Return "Does Not Match".
2280            return DoesNotMatch;
2281        }
2282    }
2283    // Step 4. If pattern is not an ASCII case-insensitive match for host, return "Does Not Match".
2284    if !ascii_case_insensitive_match(pattern, host) {
2285        return DoesNotMatch;
2286    }
2287    static IPV4_ADDRESS_RULE: Lazy<Regex> = Lazy::new(|| {
2288        Regex::new(r#"([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])"#).unwrap()
2289    });
2290    if IPV4_ADDRESS_RULE.is_match(pattern) && pattern != "127.0.0.1" {
2291        return DoesNotMatch;
2292    }
2293    // The spec uses the phrase "if A is an IPv6 address", without giving specific instructions on
2294    // how to tell if this is the case. In URLs, IPv6 addresses start with `[`, so let's go with that.
2295    // See https://url.spec.whatwg.org/#host-parsing
2296    if pattern.as_bytes()[0] == b'[' {
2297        return DoesNotMatch;
2298    }
2299    // Step 5. Return "Matches".
2300    Matches
2301}
2302
2303/// https://www.w3.org/TR/CSP/#match-ports
2304fn port_part_match(input: Option<&str>, url: &Url) -> MatchResult {
2305    use std::str::FromStr;
2306    // 1. Assert: input is null, "*", or a sequence of one or more ASCII digits.
2307    debug_assert!(input.is_none() || input == Some("*") || u16::from_str(input.unwrap()).is_ok());
2308    // 2. If input is equal to "*", return "Matches".
2309    if input == Some("*") {
2310        return Matches;
2311    }
2312    // 3. Let normalizedInput be null if input null; otherwise input interpreted as decimal number.
2313    let normalized_input = if let Some(input) = input {
2314        u16::from_str(&input).ok()
2315    } else {
2316        None
2317    };
2318    // 4. If normalizedInput equals url’s port, return "Matches".
2319    if normalized_input == url.port() {
2320        return Matches;
2321    }
2322    // 5. If url’s port is null:
2323    if url.port().is_none() {
2324        // 5.1. Let defaultPort be the default port for url’s scheme.
2325        let default_port = default_port(url.scheme());
2326        // 5.2. If normalizedInput equals defaultPort, return "Matches".
2327        if normalized_input == default_port {
2328            return Matches;
2329        }
2330    }
2331    // 6. Return "Does Not Match".
2332    DoesNotMatch
2333}
2334
2335/// https://www.w3.org/TR/CSP/#match-paths
2336fn path_part_match(path_a: &str, path_b: &str) -> MatchResult {
2337    if path_a.is_empty() {
2338        return Matches;
2339    }
2340    if path_a == "/" && path_b.is_empty() {
2341        return Matches;
2342    }
2343    let exact_match = path_a.as_bytes()[path_a.len() - 1] != b'/';
2344    let (mut path_list_a, path_list_b): (Vec<&str>, Vec<&str>) =
2345        (path_a.split('/').collect(), path_b.split('/').collect());
2346    if path_list_a.len() > path_list_b.len() {
2347        return DoesNotMatch;
2348    }
2349    if exact_match && path_list_a.len() != path_list_b.len() {
2350        return DoesNotMatch;
2351    }
2352    if !exact_match {
2353        debug_assert_eq!(path_list_a[path_list_a.len() - 1], "");
2354        path_list_a.pop();
2355    }
2356    let mut piece_b_iter = path_list_b.iter();
2357    for piece_a in &path_list_a {
2358        let piece_b = piece_b_iter.next().unwrap();
2359        let piece_a: Vec<u8> = percent_encoding::percent_decode(piece_a.as_bytes()).collect();
2360        let piece_b: Vec<u8> = percent_encoding::percent_decode(piece_b.as_bytes()).collect();
2361        if piece_a != piece_b {
2362            return DoesNotMatch;
2363        }
2364    }
2365    Matches
2366}
2367
2368fn default_port(scheme: &str) -> Option<u16> {
2369    Some(match scheme {
2370        "ftp" => 21,
2371        "gopher" => 70,
2372        "http" => 80,
2373        "https" => 443,
2374        "ws" => 80,
2375        "wss" => 443,
2376        _ => return None,
2377    })
2378}
2379
2380fn origin_scheme_part_match(a: &Origin, b: &str) -> MatchResult {
2381    if let Origin::Tuple(scheme, _host, _port) = a {
2382        scheme_part_match(&scheme[..], b)
2383    } else {
2384        DoesNotMatch
2385    }
2386}
2387
2388/// https://www.w3.org/TR/CSP/#match-schemes
2389fn scheme_part_match(a: &str, b: &str) -> MatchResult {
2390    let a = a.to_ascii_lowercase();
2391    let b = b.to_ascii_lowercase();
2392    match (&a[..], &b[..]) {
2393        _ if a == b => Matches,
2394        ("http", "https") | ("ws", "wss" | "http" | "https") | ("wss", "https") => Matches,
2395        _ => DoesNotMatch,
2396    }
2397}
2398
2399#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2400pub enum HashAlgorithm {
2401    Sha256,
2402    Sha384,
2403    Sha512,
2404}
2405
2406impl HashAlgorithm {
2407    pub fn from_name(name: &str) -> Option<HashAlgorithm> {
2408        use HashAlgorithm::*;
2409        match name {
2410            "sha256" | "Sha256" | "sHa256" | "shA256" | "SHa256" | "ShA256" | "sHA256"
2411            | "SHA256" => Some(Sha256),
2412            "sha384" | "Sha384" | "sHa384" | "shA384" | "SHa384" | "ShA384" | "sHA384"
2413            | "SHA384" => Some(Sha384),
2414            "sha512" | "Sha512" | "sHa512" | "shA512" | "SHa512" | "ShA512" | "sHA512"
2415            | "SHA512" => Some(Sha512),
2416            _ => None,
2417        }
2418    }
2419    pub fn apply(self, value: &str) -> String {
2420        use base64::Engine as _;
2421        let bytes = value.as_bytes();
2422        let standard = base64::engine::general_purpose::STANDARD;
2423        match self {
2424            HashAlgorithm::Sha256 => standard.encode(sha2::Sha256::digest(bytes)),
2425            HashAlgorithm::Sha384 => standard.encode(sha2::Sha384::digest(bytes)),
2426            HashAlgorithm::Sha512 => standard.encode(sha2::Sha512::digest(bytes)),
2427        }
2428    }
2429}
2430
2431/// https://www.w3.org/TR/SRI/#integrity-metadata
2432#[derive(Clone, Debug, Eq, PartialEq)]
2433pub struct HashFunction {
2434    algorithm: HashAlgorithm,
2435    value: String,
2436    // The spec defines a third member, options, but defines no values.
2437}
2438
2439/// https://www.w3.org/TR/SRI/#parse-metadata
2440#[derive(Clone, Debug, Eq, PartialEq)]
2441pub enum SubresourceIntegrityMetadata {
2442    NoMetadata,
2443    IntegritySources(Vec<HashFunction>),
2444}
2445
2446/// https://www.w3.org/TR/SRI/#the-integrity-attribute
2447/// This corresponds to the "hash-expression" grammar.
2448static SUBRESOURCE_METADATA_GRAMMAR: Lazy<Regex> = Lazy::new(|| {
2449    Regex::new(r#"(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)"#)
2450        .unwrap()
2451});
2452
2453/// https://www.w3.org/TR/SRI/#parse-metadata
2454pub fn parse_subresource_integrity_metadata(string: &str) -> SubresourceIntegrityMetadata {
2455    let mut result = Vec::new();
2456    let mut empty = true;
2457    for token in split_ascii_whitespace(string) {
2458        empty = false;
2459        if let Some(captures) = SUBRESOURCE_METADATA_GRAMMAR.captures(token) {
2460            if let (Some(algorithm), Some(value)) = (
2461                captures
2462                    .name("algorithm")
2463                    .and_then(|a| HashAlgorithm::from_name(a.as_str())),
2464                captures.name("value"),
2465            ) {
2466                result.push(HashFunction {
2467                    algorithm,
2468                    value: String::from(value.as_str()),
2469                });
2470            }
2471        }
2472    }
2473    if empty {
2474        SubresourceIntegrityMetadata::NoMetadata
2475    } else {
2476        SubresourceIntegrityMetadata::IntegritySources(result)
2477    }
2478}
2479
2480#[cfg(test)]
2481mod test {
2482    use super::*;
2483    #[test]
2484    fn empty_directive_is_not_valid() {
2485        let d = Directive {
2486            name: String::new(),
2487            value: Vec::new(),
2488        };
2489        assert!(!d.is_valid());
2490    }
2491    #[test]
2492    pub fn duplicate_policy_is_not_valid() {
2493        let d = Directive {
2494            name: "test".to_owned(),
2495            value: vec!["test".to_owned()],
2496        };
2497        let p = Policy {
2498            directive_set: vec![d.clone(), d.clone()],
2499            disposition: PolicyDisposition::Enforce,
2500            source: PolicySource::Header,
2501        };
2502        assert!(!p.is_valid());
2503    }
2504    #[test]
2505    pub fn basic_policy_is_valid() {
2506        let p = Policy::parse(
2507            "script-src notriddle.com",
2508            PolicySource::Header,
2509            PolicyDisposition::Enforce,
2510        );
2511        assert!(p.is_valid());
2512    }
2513    #[test]
2514    pub fn policy_with_empty_directive_set_is_not_valid() {
2515        let p = Policy {
2516            directive_set: vec![],
2517            disposition: PolicyDisposition::Enforce,
2518            source: PolicySource::Header,
2519        };
2520        assert!(!p.is_valid());
2521    }
2522
2523    #[test]
2524    pub fn prefetch_request_does_not_violate_policy() {
2525        let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2526        let request = Request {
2527            url: url.clone(),
2528            current_url: url,
2529            origin: Origin::Tuple(
2530                "https".to_string(),
2531                url::Host::Domain("notriddle.com".to_owned()),
2532                443,
2533            ),
2534            redirect_count: 0,
2535            destination: Destination::Script,
2536            initiator: Initiator::Prefetch,
2537            nonce: String::new(),
2538            integrity_metadata: String::new(),
2539            parser_metadata: ParserMetadata::None,
2540        };
2541
2542        let p = Policy::parse(
2543            "child-src 'self'",
2544            PolicySource::Header,
2545            PolicyDisposition::Enforce,
2546        );
2547
2548        let violation_result = p.does_request_violate_policy(&request);
2549
2550        assert!(violation_result == Violates::DoesNotViolate);
2551    }
2552
2553    #[test]
2554    pub fn prefetch_request_violates_policy() {
2555        let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2556        let request = Request {
2557            url: url.clone(),
2558            current_url: url,
2559            origin: Origin::Tuple(
2560                "https".to_string(),
2561                url::Host::Domain("notriddle.com".to_owned()),
2562                443,
2563            ),
2564            redirect_count: 0,
2565            destination: Destination::ServiceWorker,
2566            initiator: Initiator::None,
2567            nonce: String::new(),
2568            integrity_metadata: String::new(),
2569            parser_metadata: ParserMetadata::None,
2570        };
2571
2572        let p = Policy::parse(
2573            "default-src 'none'; script-src 'self' ",
2574            PolicySource::Header,
2575            PolicyDisposition::Enforce,
2576        );
2577
2578        let violation_result = p.does_request_violate_policy(&request);
2579
2580        let expected_result = Violates::Directive(Directive {
2581            name: String::from("script-src"),
2582            value: vec![String::from("'self'")],
2583        });
2584
2585        assert!(violation_result == expected_result);
2586    }
2587
2588    #[test]
2589    pub fn prefetch_request_is_allowed_by_directive() {
2590        let url = Url::parse("https://www.notriddle.com/script.js").unwrap();
2591        let request = Request {
2592            url: url.clone(),
2593            current_url: url,
2594            origin: Origin::Tuple(
2595                "https".to_string(),
2596                url::Host::Domain("notriddle.com".to_owned()),
2597                443,
2598            ),
2599            redirect_count: 0,
2600            destination: Destination::Script,
2601            initiator: Initiator::Prefetch,
2602            nonce: String::new(),
2603            integrity_metadata: String::new(),
2604            parser_metadata: ParserMetadata::None,
2605        };
2606
2607        let p = Policy::parse(
2608            "default-src 'none'; child-src 'self'",
2609            PolicySource::Header,
2610            PolicyDisposition::Enforce,
2611        );
2612
2613        let violation_result = p.does_request_violate_policy(&request);
2614
2615        assert!(violation_result == Violates::DoesNotViolate);
2616    }
2617
2618    #[test]
2619    pub fn websocket_request_is_allowed_by_directive() {
2620        let url = Url::parse("https://www.notriddle.com/websocket").unwrap();
2621        let request = Request {
2622            url: url.clone(),
2623            current_url: url,
2624            origin: Origin::Tuple(
2625                "https".to_string(),
2626                url::Host::Domain("notriddle.com".to_owned()),
2627                443,
2628            ),
2629            redirect_count: 0,
2630            destination: Destination::None,
2631            initiator: Initiator::None,
2632            nonce: String::new(),
2633            integrity_metadata: String::new(),
2634            parser_metadata: ParserMetadata::None,
2635        };
2636
2637        let p = Policy::parse(
2638            "connect-src ws://www.notriddle.com/websocket",
2639            PolicySource::Header,
2640            PolicyDisposition::Enforce,
2641        );
2642
2643        let violation_result = p.does_request_violate_policy(&request);
2644
2645        assert!(violation_result == Violates::DoesNotViolate);
2646    }
2647
2648    #[test]
2649    pub fn trusted_type_policy_is_valid() {
2650        let p = Policy::parse(
2651            "trusted-types 'none'",
2652            PolicySource::Meta,
2653            PolicyDisposition::Enforce,
2654        );
2655        assert!(p.is_valid());
2656        assert_eq!(p.directive_set[0].value, vec!["'none'".to_owned()]);
2657    }
2658
2659    #[test]
2660    pub fn csp_list_is_valid() {
2661        let csp_list = CspList::parse(
2662            "default-src 'none'; child-src 'self', trusted-types 'none'",
2663            PolicySource::Meta,
2664            PolicyDisposition::Enforce,
2665        );
2666        assert!(csp_list.is_valid());
2667        assert_eq!(
2668            csp_list.0[1].directive_set[0].value,
2669            vec!["'none'".to_owned()]
2670        );
2671    }
2672
2673    #[test]
2674    pub fn no_trusted_types_specified_allows_all_policies() {
2675        let csp_list = CspList::parse(
2676            "default-src 'none'; child-src 'self'",
2677            PolicySource::Meta,
2678            PolicyDisposition::Enforce,
2679        );
2680        assert!(csp_list.is_valid());
2681        let (check_result, violations) =
2682            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2683        assert_eq!(check_result, CheckResult::Allowed);
2684        assert!(violations.is_empty());
2685    }
2686
2687    #[test]
2688    pub fn none_does_not_allow_for_any_policy() {
2689        let csp_list = CspList::parse(
2690            "trusted-types 'none'",
2691            PolicySource::Meta,
2692            PolicyDisposition::Enforce,
2693        );
2694        assert!(csp_list.is_valid());
2695        let (check_result, violations) =
2696            csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2697        assert!(check_result == CheckResult::Blocked);
2698        assert_eq!(violations.len(), 1);
2699    }
2700
2701    #[test]
2702    pub fn extra_none_allows_all_policies() {
2703        let csp_list = CspList::parse(
2704            "trusted-types some-policy 'none'",
2705            PolicySource::Meta,
2706            PolicyDisposition::Enforce,
2707        );
2708        assert!(csp_list.is_valid());
2709        let (check_result, violations) =
2710            csp_list.is_trusted_type_policy_creation_allowed("some-policy", &[]);
2711        assert!(check_result == CheckResult::Allowed);
2712        assert!(violations.is_empty());
2713    }
2714
2715    #[test]
2716    pub fn explicit_policy_named_is_allowed() {
2717        let csp_list = CspList::parse(
2718            "trusted-types MyPolicy",
2719            PolicySource::Meta,
2720            PolicyDisposition::Enforce,
2721        );
2722        assert!(csp_list.is_valid());
2723        let (check_result, violations) =
2724            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2725        assert_eq!(check_result, CheckResult::Allowed);
2726        assert!(violations.is_empty());
2727    }
2728
2729    #[test]
2730    pub fn other_policy_name_is_blocked() {
2731        let csp_list = CspList::parse(
2732            "trusted-types MyPolicy",
2733            PolicySource::Meta,
2734            PolicyDisposition::Enforce,
2735        );
2736        assert!(csp_list.is_valid());
2737        let (check_result, violations) =
2738            csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2739        assert!(check_result == CheckResult::Blocked);
2740        assert_eq!(violations.len(), 1);
2741    }
2742
2743    #[test]
2744    pub fn invalid_characters_in_policy_name_is_blocked() {
2745        let csp_list = CspList::parse(
2746            "trusted-types My?Policy",
2747            PolicySource::Meta,
2748            PolicyDisposition::Enforce,
2749        );
2750        assert!(csp_list.is_valid());
2751        let (check_result, violations) =
2752            csp_list.is_trusted_type_policy_creation_allowed("My?Policy", &["My?Policy"]);
2753        assert!(check_result == CheckResult::Blocked);
2754        assert_eq!(violations.len(), 1);
2755    }
2756
2757    #[test]
2758    pub fn already_created_policy_is_blocked() {
2759        let csp_list = CspList::parse(
2760            "trusted-types MyPolicy",
2761            PolicySource::Meta,
2762            PolicyDisposition::Enforce,
2763        );
2764        assert!(csp_list.is_valid());
2765        let (check_result, violations) =
2766            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2767        assert!(check_result == CheckResult::Blocked);
2768        assert_eq!(violations.len(), 1);
2769    }
2770
2771    #[test]
2772    pub fn already_created_policy_is_allowed_with_allow_duplicates() {
2773        let csp_list = CspList::parse(
2774            "trusted-types MyPolicy 'allow-duplicates'",
2775            PolicySource::Meta,
2776            PolicyDisposition::Enforce,
2777        );
2778        assert!(csp_list.is_valid());
2779        let (check_result, violations) =
2780            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2781        assert!(check_result == CheckResult::Allowed);
2782        assert!(violations.is_empty());
2783    }
2784
2785    #[test]
2786    pub fn only_report_policy_issues_for_disposition_report() {
2787        let csp_list = CspList::parse(
2788            "trusted-types MyPolicy",
2789            PolicySource::Meta,
2790            PolicyDisposition::Report,
2791        );
2792        assert!(csp_list.is_valid());
2793        let (check_result, violations) =
2794            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &["MyPolicy"]);
2795        assert!(check_result == CheckResult::Allowed);
2796        assert_eq!(violations.len(), 1);
2797    }
2798
2799    #[test]
2800    pub fn wildcard_allows_all_policies() {
2801        let csp_list = CspList::parse(
2802            "trusted-types *",
2803            PolicySource::Meta,
2804            PolicyDisposition::Report,
2805        );
2806        assert!(csp_list.is_valid());
2807        let (check_result, violations) =
2808            csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &[]);
2809        assert!(check_result == CheckResult::Allowed);
2810        assert!(violations.is_empty());
2811    }
2812
2813    #[test]
2814    pub fn violation_has_correct_directive() {
2815        let csp_list = CspList::parse(
2816            "trusted-types MyPolicy",
2817            PolicySource::Meta,
2818            PolicyDisposition::Enforce,
2819        );
2820        assert!(csp_list.is_valid());
2821        let (check_result, violations) =
2822            csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &[]);
2823        assert!(check_result == CheckResult::Blocked);
2824        assert_eq!(violations.len(), 1);
2825        assert_eq!(violations[0].directive, csp_list.0[0].directive_set[0]);
2826    }
2827
2828    #[test]
2829    pub fn long_policy_name_is_truncated() {
2830        let csp_list = CspList::parse(
2831            "trusted-types MyPolicy",
2832            PolicySource::Meta,
2833            PolicyDisposition::Enforce,
2834        );
2835        assert!(csp_list.is_valid());
2836        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed(
2837            "SuperLongPolicyNameThatExceeds40Characters",
2838            &[],
2839        );
2840        assert!(check_result == CheckResult::Blocked);
2841        assert_eq!(violations.len(), 1);
2842        assert!(
2843            matches!(&violations[0].resource, ViolationResource::TrustedTypePolicy { sample } if sample == "SuperLongPolicyNameThatExceeds40Characte")
2844        );
2845    }
2846}