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