1#[derive(Debug, Clone)]
5pub struct DomainAllowlist {
6 patterns: Vec<AllowlistEntry>,
7}
8
9#[derive(Debug, Clone)]
10enum AllowlistEntry {
11 Exact(String),
12 Wildcard(String), }
14
15impl DomainAllowlist {
16 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 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 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}