rama_http/matcher/
subdomain_trie.rs1use crate::Request;
2use rama_core::telemetry::tracing;
3use rama_core::{extensions::Extensions, matcher::Matcher};
4use rama_net::AuthorityInputExt;
5use rama_net::address::{AsDomainRef, DomainTrie};
6
7#[derive(Debug, Clone)]
8pub struct SubdomainTrieMatcher {
12 trie: DomainTrie<()>,
13}
14
15impl SubdomainTrieMatcher {
16 pub fn new<I, S>(domains: I) -> Self
24 where
25 I: IntoIterator<Item = S>,
26 S: AsDomainRef,
27 {
28 let mut trie = DomainTrie::new();
29 for d in domains {
30 if let Ok(w) = d.to_wildcard() {
31 trie.insert_domain(w, ());
32 }
33 }
34 Self { trie }
35 }
36
37 pub fn is_match(&self, domain: impl AsDomainRef) -> bool {
39 self.trie.is_match(domain)
40 }
41}
42
43impl<Body> Matcher<Request<Body>> for SubdomainTrieMatcher {
44 fn matches(&self, _: Option<&Extensions>, req: &Request<Body>) -> bool {
45 let Some(authority) = req.authority() else {
46 tracing::debug!("SubdomainTrieMatcher: failed to resolve authority");
47 return false;
48 };
49
50 if authority.host.try_as_ip().is_ok() {
55 tracing::trace!("SubdomainTrieMatcher: host is an IP — no match");
56 return false;
57 }
58 let Ok(domain) = authority.host.try_as_domain() else {
61 tracing::trace!("SubdomainTrieMatcher: host is not a domain — no match");
62 return false;
63 };
64 let is_match = self.is_match(domain.as_ref());
65 tracing::trace!(
66 "SubdomainTrieMatcher: matching domain = {}, matched = {}",
67 domain,
68 is_match
69 );
70 is_match
71 }
72}
73
74impl<S> FromIterator<S> for SubdomainTrieMatcher
75where
76 S: AsDomainRef,
77{
78 #[inline]
79 fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
80 Self::new(iter)
81 }
82}
83
84#[cfg(test)]
85mod subdomain_trie_tests {
86 use super::*;
87 use rama_net::uri::Uri;
88
89 #[test]
90 fn test_trie_matching() {
91 let matcher = SubdomainTrieMatcher::new(vec!["example.com", "sub.domain.org"]);
92 assert!(matcher.is_match("example.com"));
93 assert!(matcher.is_match(".example.com"));
94 assert!(matcher.is_match("sub.domain.org"));
95 assert!(matcher.is_match("sub.example.com"));
96 assert!(!matcher.is_match("domain.org"));
97 assert!(!matcher.is_match("other.com"));
98 assert!(!matcher.is_match("localhost"));
99 }
100
101 #[test]
102 fn test_path_matching_with_trie() {
103 let domains = ["example.com", "sub.domain.org"];
104 let matcher: SubdomainTrieMatcher = domains.into_iter().collect();
105
106 let path = "sub.example.com";
107
108 let request = Request::builder()
109 .uri(Uri::parse_authority_form(path).unwrap())
110 .body(())
111 .unwrap();
112 assert!(matcher.matches(None, &request));
113 }
114
115 #[test]
116 fn test_non_matching_path() {
117 let domains = ["example.com"];
118 let matcher: SubdomainTrieMatcher = domains.into_iter().collect();
119
120 let path = "nonmatching.com";
121
122 let request = Request::builder()
123 .uri(Uri::parse_authority_form(path).unwrap())
124 .body(())
125 .unwrap();
126 assert!(!matcher.matches(None, &request));
127 }
128
129 fn req_with_host(host_header: &str) -> Request<()> {
130 Request::builder()
131 .uri("/")
132 .header("host", host_header)
133 .body(())
134 .unwrap()
135 }
136
137 #[test]
138 fn pct_encoded_reg_name_matches_via_bridge() {
139 let matcher: SubdomainTrieMatcher = ["example.com"].into_iter().collect();
143 assert!(matcher.matches(None, &req_with_host("exa%6Dple.com")));
144 }
145
146 #[test]
147 fn ip_host_does_not_match() {
148 let matcher: SubdomainTrieMatcher = ["127.0.0.1"].into_iter().collect();
149 assert!(!matcher.matches(None, &req_with_host("127.0.0.1")));
150 assert!(!matcher.matches(None, &req_with_host("%31%32%37.0.0.1")));
154 }
155}