1pub mod matcher;
2pub mod parser;
3pub mod prelude;
4pub mod registry;
5
6pub use matcher::Matcher;
7pub use parser::parse_rule;
8pub use registry::{parse_custom_rules, register_router_rule};
9
10#[cfg(test)]
11mod tests {
12 use super::parse_rule;
13 use crate::matcher::Matcher;
14 use crate::registry::register_router_rule;
15 use pingora::http::RequestHeader;
16 use winnow::ascii::multispace0;
17 use winnow::token::literal as tag;
18 use winnow::{Parser, Result};
19
20 #[test]
21 fn test_rule_parsing_and_matching() {
22 let mut req = RequestHeader::build("GET", b"/api/v1/users", None).unwrap();
24 req.insert_header("Host", "example.com").unwrap();
25
26 let rule = parse_rule("Host(`example.com`)").unwrap();
28 assert!(rule.matches(&req));
29
30 let rule = parse_rule("Host(`other.com`)").unwrap();
31 assert!(!rule.matches(&req));
32
33 let rule = parse_rule("PathPrefix(`/api`)").unwrap();
35 assert!(rule.matches(&req));
36
37 let rule = parse_rule("Host(`example.com`) && PathPrefix(`/api`)").unwrap();
39 assert!(rule.matches(&req));
40
41 let rule = parse_rule("Host(`other.com`) || PathPrefix(`/api`)").unwrap();
43 assert!(rule.matches(&req));
44
45 let rule =
47 parse_rule("(Host(`example.com`) && Path(`/foo`)) || PathPrefix(`/api`)").unwrap();
48 assert!(rule.matches(&req));
49 }
50
51 #[test]
52 fn test_regex_and_not_matching() {
53 let mut req = RequestHeader::build("GET", b"/api/v1/users", None).unwrap();
54 req.insert_header("Host", "sub.example.com").unwrap();
55
56 let rule = parse_rule("HostRegexp(`.*\\.example\\.com`)").unwrap();
58 assert!(rule.matches(&req));
59
60 let rule = parse_rule("HostRegexp(`^example\\.com$`)").unwrap();
61 assert!(!rule.matches(&req));
62
63 let rule = parse_rule("PathRegexp(`^/api/v\\d+/.*`)").unwrap();
65 assert!(rule.matches(&req));
66
67 let rule = parse_rule("!Host(`other.com`)").unwrap();
69 assert!(rule.matches(&req));
70
71 let rule = parse_rule("!Host(`sub.example.com`)").unwrap();
72 assert!(!rule.matches(&req));
73
74 let rule = parse_rule("HostRegexp(`.*\\.example\\.com`) && !PathPrefix(`/admin`)").unwrap();
76 assert!(rule.matches(&req));
77 }
78
79 #[derive(Debug)]
80 struct CustomMatcher;
81 impl Matcher for CustomMatcher {
82 fn matches(&self, _req: &RequestHeader) -> bool {
83 true
84 }
85
86 fn clone_box(&self) -> Box<dyn Matcher> {
87 Box::new(CustomMatcher)
88 }
89 }
90
91 #[test]
92 fn test_custom_rule_registry() {
93 fn parse_my_rule(input: &mut &str) -> Result<Box<dyn Matcher>> {
95 (tag("MyRule"), multispace0, '(', ')')
96 .map(|_| Box::new(CustomMatcher) as Box<dyn Matcher>)
97 .parse_next(input)
98 }
99
100 register_router_rule(parse_my_rule);
102
103 let rule = parse_rule("MyRule()").unwrap();
105
106 let req = RequestHeader::build("GET", b"/", None).unwrap();
108 assert!(rule.matches(&req));
109 }
110
111 #[test]
112 fn test_header_and_query_regexp() {
113 let rule = parse_rule("HeaderRegexp(`User-Agent`, `^Mozilla.*`)").unwrap();
115 let mut req = RequestHeader::build("GET", b"/", None).unwrap();
116 req.insert_header(
117 "User-Agent",
118 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
119 )
120 .unwrap();
121 assert!(rule.matches(&req));
122
123 req.insert_header("User-Agent", "Curl/7.64.1").unwrap();
124 assert!(!rule.matches(&req));
125
126 let rule = parse_rule("QueryRegexp(`id`, `^[0-9]+$`)").unwrap();
128 let req_match = RequestHeader::build("GET", b"/path?id=123", None).unwrap();
129 assert!(rule.matches(&req_match));
130
131 let req_no_match = RequestHeader::build("GET", b"/path?id=abc", None).unwrap();
132 assert!(!rule.matches(&req_no_match));
133 }
134}