Skip to main content

rama_http/matcher/
uri.rs

1//! provides a [`UriMatcher`] matcher for matching requests based on their URI.
2
3use crate::Request;
4use rama_core::{extensions::Extensions, telemetry::tracing};
5use rama_net::uri::Uri;
6use rama_utils::collections::smallvec::SmallVec;
7use rama_utils::thirdparty::{regex::Regex, wildcard::Wildcard};
8use std::io::Write as _;
9
10#[derive(Debug, Clone)]
11/// Matcher the request's URI, using a substring or regex pattern.
12pub struct UriMatcher {
13    engine: Engine,
14}
15
16#[derive(Debug, Clone)]
17enum Engine {
18    Re(Regex),
19    Wc(Wildcard<'static>),
20}
21
22impl Engine {
23    fn is_match(&self, s: &str) -> bool {
24        match self {
25            Self::Re(regex) => regex.is_match(s),
26            Self::Wc(wildcard) => wildcard.is_match(s.as_bytes()),
27        }
28    }
29
30    fn is_match_bytes(&self, bytes: &[u8]) -> bool {
31        match self {
32            Self::Re(regex) => std::str::from_utf8(bytes).map(|s| regex.is_match(s)).inspect_err(|err| {
33                tracing::debug!(
34                    "input byttes is not a valid utf-8 str: regex requires str: fallback to is_match=false; err = {err}",
35                );
36            }).unwrap_or_default(),
37            Self::Wc(wildcard) => wildcard.is_match(bytes),
38        }
39    }
40}
41
42impl UriMatcher {
43    #[must_use]
44    /// create a new Uri matcher using a regex pattern.
45    pub fn regex(re: Regex) -> Self {
46        Self {
47            engine: Engine::Re(re),
48        }
49    }
50
51    #[must_use]
52    /// create a new Uri matcher using a wildcard pattern.
53    pub fn wildcard(wc: Wildcard<'static>) -> Self {
54        Self {
55            engine: Engine::Wc(wc),
56        }
57    }
58
59    #[inline]
60    pub(crate) fn matches_uri(&self, uri: &Uri) -> bool {
61        match uri.authority() {
62            Some(authority) => {
63                let mut buffer = SmallVec::<[u8; 128]>::new();
64                _ = write!(
65                    &mut buffer,
66                    "{}://{authority}{}",
67                    uri.scheme_str().unwrap_or("http"),
68                    uri.path_or_root().as_ref()
69                );
70                while buffer.last() == Some(&b'/') {
71                    _ = buffer.pop();
72                }
73                self.engine.is_match_bytes(&buffer)
74            }
75            None => self.engine.is_match(uri.path_or_root().as_ref()),
76        }
77    }
78}
79
80impl From<Regex> for UriMatcher {
81    fn from(re: Regex) -> Self {
82        Self {
83            engine: Engine::Re(re),
84        }
85    }
86}
87
88impl From<Wildcard<'static>> for UriMatcher {
89    fn from(wc: Wildcard<'static>) -> Self {
90        Self {
91            engine: Engine::Wc(wc),
92        }
93    }
94}
95
96impl<Body> rama_core::matcher::Matcher<Request<Body>> for UriMatcher {
97    fn matches(&self, _ext: Option<&Extensions>, req: &Request<Body>) -> bool {
98        let uri = req.request_uri();
99        // TODO: in future we probably do not want to go via request_uri,
100        // as this allocates an entire uri even though we do not want query etc...
101        self.matches_uri(&uri)
102    }
103}
104
105#[cfg(test)]
106mod test {
107    use crate::header::HOST;
108    use rama_core::matcher::Matcher as _;
109
110    use super::*;
111
112    #[test]
113    fn matchest_uri_regex_match() {
114        for (matcher, uri) in [
115            (r"www\.example\.com", "http://www.example.com"),
116            (r"(?i)www\.example\.com", "http://WwW.ExamplE.COM"),
117            (
118                r"(?i)^[^?]+\.(jpeg|png|gif|css)$",
119                "http://www.example.com/assets/style.css?foo=bar",
120            ),
121            (
122                r"(?i)^[^?]+\.(jpeg|png|gif|css)$",
123                "http://www.example.com/image.png",
124            ),
125        ] {
126            let matcher = UriMatcher::regex(Regex::new(matcher).unwrap());
127            assert!(
128                matcher.matches_uri(&(uri.parse().unwrap())),
129                "({matcher:?}).matches_uri({uri})",
130            );
131        }
132    }
133
134    #[test]
135    fn matchest_uri_wildcard_match() {
136        for (matcher, uri) in [
137            (r"*www.example.com", "http://www.example.com"),
138            (r"*.css", "http://www.example.com/assets/style.css"),
139            (r"*.css", "http://www.example.com/assets/style.css?foo=bar"),
140            (
141                r"*example.com/foo/*/baz",
142                "http://www.example.com/foo/bar/42/baz",
143            ),
144        ] {
145            let matcher = UriMatcher::wildcard(Wildcard::new(matcher.as_bytes()).unwrap());
146            assert!(
147                matcher.matches_uri(&(uri.parse().unwrap())),
148                "({matcher:?}).matches_uri({uri})",
149            );
150        }
151    }
152
153    #[test]
154    fn matchest_uri_regex_no_match() {
155        for (matcher, uri) in [
156            ("www.example.com", "http://WwW.ExamplE.COM"),
157            (
158                r"(?i)^[^?]+\.(jpeg|png|gif|css)(\?|\z)",
159                "http://www.example.com/?style.css",
160            ),
161        ] {
162            let matcher = UriMatcher::regex(Regex::new(matcher).unwrap());
163            assert!(
164                !matcher.matches_uri(&(uri.parse().unwrap())),
165                "!({matcher:?}).matches_uri({uri})",
166            );
167        }
168    }
169
170    #[test]
171    fn matchest_uri_wildcard_no_match() {
172        for (matcher, uri) in [
173            ("http://example.com", "www.example.com"),
174            (r"*.png", "http://www.example.com/style.css"),
175        ] {
176            let matcher = UriMatcher::wildcard(Wildcard::new(matcher.as_bytes()).unwrap());
177            // accept both absolute and bare authority-form test targets
178            let uri_parsed = uri
179                .parse::<Uri>()
180                .or_else(|_| Uri::parse_authority_form(uri))
181                .unwrap();
182            assert!(
183                !matcher.matches_uri(&uri_parsed),
184                "!({matcher:?}).matches_uri({uri})",
185            );
186        }
187    }
188
189    #[test]
190    fn uri_matches_regex_req() {
191        for (matcher, req) in [
192            (
193                r"(?i)http://www\.example\.com",
194                Request::builder()
195                    .uri(Uri::parse_authority_form("WwW.ExamplE.COM").unwrap())
196                    .body(())
197                    .unwrap(),
198            ),
199            (
200                r"(?i)^[^?]+\.(jpeg|png|gif|css)(\?|\z)",
201                Request::builder()
202                    .uri("http://www.example.com/assets/style.css?foo=bar")
203                    .body(())
204                    .unwrap(),
205            ),
206            (
207                "/foo/bar",
208                Request::builder().uri("/foo/bar").body(()).unwrap(),
209            ),
210            (
211                "example.com/foo/bar",
212                Request::builder()
213                    .uri("/foo/bar")
214                    .header(HOST, "example.com")
215                    .body(())
216                    .unwrap(),
217            ),
218        ] {
219            let matcher = UriMatcher::regex(Regex::new(matcher).unwrap());
220            assert!(
221                matcher.matches(None, &req),
222                "matcher: {matcher:?}; req: {req:?}"
223            );
224        }
225    }
226
227    #[test]
228    fn uri_matches_wildcard_req() {
229        for (matcher, req) in [
230            (
231                r"*://www.example.com",
232                Request::builder()
233                    .uri(Uri::parse_authority_form("www.example.com").unwrap())
234                    .body(())
235                    .unwrap(),
236            ),
237            (
238                r"*/*.css",
239                Request::builder()
240                    .uri("http://www.example.com/assets/style.css?foo=bar")
241                    .body(())
242                    .unwrap(),
243            ),
244            (
245                "/foo/bar",
246                Request::builder().uri("/foo/bar").body(()).unwrap(),
247            ),
248            (
249                "http://example.com/*/bar",
250                Request::builder()
251                    .uri("/foo/bar")
252                    .header(HOST, "example.com")
253                    .body(())
254                    .unwrap(),
255            ),
256        ] {
257            let matcher = UriMatcher::wildcard(Wildcard::new(matcher.as_bytes()).unwrap());
258            assert!(
259                matcher.matches(None, &req),
260                "matcher: {matcher:?}; req: {req:?}"
261            );
262        }
263    }
264}