Skip to main content

net_traits/
pub_domains.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Implementation of public domain matching.
6//!
7//! The list is a file located on the `resources` folder and loaded once on first need.
8//!
9//! The list can be updated with `./mach update-pub-domains` from this source:
10//! <https://publicsuffix.org/list/>
11//!
12//! This implementation is not strictly following the specification of the list. Wildcards are not
13//! restricted to appear only in the leftmost position, but the current list has no such cases so
14//! we don't need to make the code more complex for it. The `mach` update command makes sure that
15//! those cases are not present.
16
17use std::iter::FromIterator;
18use std::sync::LazyLock;
19
20use embedder_traits::resources::{self, Resource};
21use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
22use malloc_size_of_derive::MallocSizeOf;
23use rustc_hash::FxHashSet;
24use serde::{Deserialize, Serialize};
25use servo_url::{Host, ImmutableOrigin, ServoUrl};
26
27// We can use FxHash here.
28// The list is given by publicsuffix.org so an attack is highly unlikely
29#[derive(Clone, Debug, Default, MallocSizeOf)]
30pub struct PubDomainRules {
31    rules: FxHashSet<String>,
32    wildcards: FxHashSet<String>,
33    exceptions: FxHashSet<String>,
34}
35
36static PUB_DOMAINS: LazyLock<PubDomainRules> = LazyLock::new(load_pub_domains);
37
38pub fn public_suffix_list_size_of(ops: &mut MallocSizeOfOps) -> usize {
39    PUB_DOMAINS.size_of(ops)
40}
41
42impl<'a> FromIterator<&'a str> for PubDomainRules {
43    fn from_iter<T>(iter: T) -> Self
44    where
45        T: IntoIterator<Item = &'a str>,
46    {
47        let mut result = PubDomainRules::default();
48        for item in iter {
49            if let Some(stripped) = item.strip_prefix('!') {
50                result.exceptions.insert(String::from(stripped));
51            } else if let Some(stripped) = item.strip_prefix("*.") {
52                result.wildcards.insert(String::from(stripped));
53            } else {
54                result.rules.insert(String::from(item));
55            }
56        }
57        result
58    }
59}
60
61impl PubDomainRules {
62    pub fn parse(content: &str) -> PubDomainRules {
63        content
64            .lines()
65            .map(str::trim)
66            .filter(|s| !s.is_empty())
67            .filter(|s| !s.starts_with("//"))
68            .collect()
69    }
70    fn suffix_pair<'a>(&self, domain: &'a str) -> (&'a str, &'a str) {
71        let domain = domain.trim_start_matches('.');
72        let mut suffix = domain;
73        let mut prev_suffix = domain;
74        for (index, _) in domain.match_indices('.') {
75            let next_suffix = &domain[index + 1..];
76            if self.exceptions.contains(suffix) {
77                return (next_suffix, suffix);
78            }
79            if self.wildcards.contains(next_suffix) || self.rules.contains(suffix) {
80                return (suffix, prev_suffix);
81            }
82            prev_suffix = suffix;
83            suffix = next_suffix;
84        }
85        (suffix, prev_suffix)
86    }
87    pub fn public_suffix<'a>(&self, domain: &'a str) -> &'a str {
88        let (public, _) = self.suffix_pair(domain);
89        public
90    }
91    pub fn registrable_suffix<'a>(&self, domain: &'a str) -> &'a str {
92        let (_, registrable) = self.suffix_pair(domain);
93        registrable
94    }
95    pub fn is_public_suffix(&self, domain: &str) -> bool {
96        // Speeded-up version of
97        // domain != "" &&
98        // self.public_suffix(domain) == domain.
99        let domain = domain.trim_start_matches('.');
100        match domain.find('.') {
101            None => !domain.is_empty(),
102            Some(index) => {
103                !self.exceptions.contains(domain) && self.wildcards.contains(&domain[index + 1..]) ||
104                    self.rules.contains(domain)
105            },
106        }
107    }
108    pub fn is_registrable_suffix(&self, domain: &str) -> bool {
109        // Speeded-up version of
110        // self.public_suffix(domain) != domain &&
111        // self.registrable_suffix(domain) == domain.
112        let domain = domain.trim_start_matches('.');
113        match domain.find('.') {
114            None => false,
115            Some(index) => {
116                self.exceptions.contains(domain) ||
117                    !self.wildcards.contains(&domain[index + 1..]) &&
118                        !self.rules.contains(domain) &&
119                        self.is_public_suffix(&domain[index + 1..])
120            },
121        }
122    }
123}
124
125fn load_pub_domains() -> PubDomainRules {
126    PubDomainRules::parse(&resources::read_string(Resource::DomainList))
127}
128
129pub fn pub_suffix(domain: &str) -> &str {
130    PUB_DOMAINS.public_suffix(domain)
131}
132
133pub fn reg_suffix(domain: &str) -> &str {
134    PUB_DOMAINS.registrable_suffix(domain)
135}
136
137pub fn is_pub_domain(domain: &str) -> bool {
138    PUB_DOMAINS.is_public_suffix(domain)
139}
140
141pub fn is_reg_domain(domain: &str) -> bool {
142    PUB_DOMAINS.is_registrable_suffix(domain)
143}
144
145/// <https://html.spec.whatwg.org/multipage/#same-site>
146pub fn is_same_site(site_a: &ImmutableOrigin, site_b: &ImmutableOrigin) -> bool {
147    // First steps are for
148    // https://html.spec.whatwg.org/multipage/#concept-site-same-site
149    //
150    // Step 1. If A and B are the same opaque origin, then return true.
151    if !site_a.is_tuple() && !site_b.is_tuple() && site_a == site_b {
152        return true;
153    }
154
155    // Step 2. If A or B is an opaque origin, then return false.
156    let ImmutableOrigin::Tuple(scheme_a, host_a, _) = site_a else {
157        return false;
158    };
159    let ImmutableOrigin::Tuple(scheme_b, host_b, _) = site_b else {
160        return false;
161    };
162
163    // Step 3. If A's and B's scheme values are different, then return false.
164    if scheme_a != scheme_b {
165        return false;
166    }
167
168    // Step 4. If A's and B's host values are not equal, then return false.
169    // Includes the steps of https://html.spec.whatwg.org/multipage/#obtain-a-site
170    if let (Host::Domain(domain_a), Host::Domain(domain_b)) = (host_a, host_b) {
171        if reg_suffix(domain_a) != reg_suffix(domain_b) {
172            return false;
173        }
174    } else if host_a != host_b {
175        return false;
176    }
177
178    // Step 5. Return true.
179    true
180}
181
182/// The registered domain name (aka eTLD+1) for a URL.
183/// Returns None if the URL has no host name.
184/// Returns the registered suffix for the host name if it is a domain.
185/// Leaves the host name alone if it is an IP address.
186pub fn registered_domain_name(url: &ServoUrl) -> Option<Host> {
187    match url.origin() {
188        ImmutableOrigin::Tuple(_, Host::Domain(domain), _) => {
189            Some(Host::Domain(String::from(reg_suffix(&domain))))
190        },
191        ImmutableOrigin::Tuple(_, ip, _) => Some(ip),
192        ImmutableOrigin::Opaque(_) => None,
193    }
194}
195
196/// A [site](https://html.spec.whatwg.org/multipage/#site).
197#[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
198pub enum Site {
199    Opaque(ImmutableOrigin),
200    Tuple { scheme: String, host: Host },
201}
202
203impl Site {
204    /// <https://html.spec.whatwg.org/multipage/#obtain-a-site>
205    pub fn from_origin(origin: &ImmutableOrigin) -> Self {
206        match origin {
207            // Step 1. If origin is an opaque origin, then return origin.
208            ImmutableOrigin::Opaque(_) => Site::Opaque(origin.clone()),
209            // Step 3. Return (origin's scheme, origin's host's registrable domain).
210            ImmutableOrigin::Tuple(scheme, Host::Domain(domain), _) => Site::Tuple {
211                scheme: scheme.clone(),
212                host: Host::Domain(reg_suffix(domain).to_owned()),
213            },
214            // Step 2. If origin's host's registrable domain is null, then return (origin's scheme, origin's host).
215            ImmutableOrigin::Tuple(scheme, host, _) => Site::Tuple {
216                scheme: scheme.clone(),
217                host: host.clone(),
218            },
219        }
220    }
221}