1use 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)]
11pub 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 pub fn regex(re: Regex) -> Self {
46 Self {
47 engine: Engine::Re(re),
48 }
49 }
50
51 #[must_use]
52 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 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 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}