Skip to main content

sbe_proxy/
allowlist.rs

1/// Domain allowlist for the proxy.
2///
3/// Supports exact matches and wildcard prefix patterns (`*.example.com`).
4#[derive(Debug, Clone)]
5pub struct DomainAllowlist {
6    patterns: Vec<AllowlistEntry>,
7}
8
9#[derive(Debug, Clone)]
10enum AllowlistEntry {
11    Exact(String),
12    Wildcard(String), // stores the suffix (e.g., "example.com" for "*.example.com")
13}
14
15impl DomainAllowlist {
16    /// Create a new allowlist from domain pattern strings.
17    ///
18    /// Patterns can be exact (`"registry.npmjs.org"`) or wildcard (`"*.npmjs.org"`).
19    pub fn new(patterns: &[String]) -> Self {
20        let patterns = patterns
21            .iter()
22            .map(|p| {
23                if let Some(suffix) = p.strip_prefix("*.") {
24                    AllowlistEntry::Wildcard(suffix.to_lowercase())
25                } else {
26                    AllowlistEntry::Exact(p.to_lowercase())
27                }
28            })
29            .collect();
30        Self { patterns }
31    }
32
33    /// Check whether a domain is allowed.
34    pub fn is_allowed(&self, domain: &str) -> bool {
35        let domain = domain.to_lowercase();
36        self.patterns.iter().any(|entry| match entry {
37            AllowlistEntry::Exact(d) => domain == *d,
38            AllowlistEntry::Wildcard(suffix) => {
39                domain == *suffix || domain.ends_with(&format!(".{suffix}"))
40            }
41        })
42    }
43
44    /// Check if the allowlist is empty (no domains allowed).
45    pub fn is_empty(&self) -> bool {
46        self.patterns.is_empty()
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_should_allow_exact_match() {
56        let al = DomainAllowlist::new(&["registry.npmjs.org".to_owned()]);
57        assert!(al.is_allowed("registry.npmjs.org"));
58        assert!(!al.is_allowed("evil.com"));
59    }
60
61    #[test]
62    fn test_should_allow_wildcard_match() {
63        let al = DomainAllowlist::new(&["*.npmjs.org".to_owned()]);
64        assert!(al.is_allowed("registry.npmjs.org"));
65        assert!(al.is_allowed("npmjs.org"));
66        assert!(!al.is_allowed("evil.com"));
67    }
68
69    #[test]
70    fn test_should_be_case_insensitive() {
71        let al = DomainAllowlist::new(&["Registry.NPMJS.org".to_owned()]);
72        assert!(al.is_allowed("registry.npmjs.org"));
73        assert!(al.is_allowed("REGISTRY.NPMJS.ORG"));
74    }
75
76    #[test]
77    fn test_should_handle_empty_allowlist() {
78        let al = DomainAllowlist::new(&[]);
79        assert!(!al.is_allowed("anything.com"));
80        assert!(al.is_empty());
81    }
82}