1use ipnet::IpNet;
11use serde::{Deserialize, Serialize};
12use std::net::IpAddr;
13use std::str::FromStr;
14
15#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
17pub struct HeaderMatch {
18 pub name: String,
20 #[serde(default)]
22 pub value: Option<String>,
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
27pub struct RequestMatcher {
28 #[serde(default)]
32 pub source_ips: Vec<String>,
33
34 #[serde(default)]
36 pub headers: Vec<HeaderMatch>,
37
38 #[serde(default)]
40 pub min_body_size_bytes: Option<usize>,
41
42 #[serde(default)]
44 pub max_body_size_bytes: Option<usize>,
45
46 #[serde(default)]
50 pub chunked_only: Option<bool>,
51}
52
53impl RequestMatcher {
54 pub fn is_empty(&self) -> bool {
56 self.source_ips.is_empty()
57 && self.headers.is_empty()
58 && self.min_body_size_bytes.is_none()
59 && self.max_body_size_bytes.is_none()
60 && self.chunked_only.is_none()
61 }
62
63 pub fn matches<'a, I>(
70 &self,
71 client_ip: Option<&str>,
72 headers: I,
73 body_size: Option<usize>,
74 is_chunked: bool,
75 ) -> bool
76 where
77 I: IntoIterator<Item = (&'a str, &'a str)> + Clone,
78 {
79 if self.is_empty() {
80 return true;
81 }
82
83 if !self.source_ips.is_empty() {
84 let ok = client_ip
85 .and_then(|s| IpAddr::from_str(s).ok())
86 .map(|ip| self.source_ips.iter().any(|cidr| ip_in_cidr(ip, cidr)))
87 .unwrap_or(false);
88 if !ok {
89 return false;
90 }
91 }
92
93 for hm in &self.headers {
94 let needle = hm.name.to_ascii_lowercase();
95 let mut found = false;
96 for (k, v) in headers.clone() {
97 if k.eq_ignore_ascii_case(&needle) {
98 match &hm.value {
99 None => {
100 found = true;
101 break;
102 }
103 Some(expected) if v == expected => {
104 found = true;
105 break;
106 }
107 _ => continue,
108 }
109 }
110 }
111 if !found {
112 return false;
113 }
114 }
115
116 if let Some(min) = self.min_body_size_bytes {
117 if body_size.unwrap_or(0) < min {
118 return false;
119 }
120 }
121 if let Some(max) = self.max_body_size_bytes {
122 if body_size.unwrap_or(0) > max {
123 return false;
124 }
125 }
126
127 if let Some(want) = self.chunked_only {
128 if want != is_chunked {
129 return false;
130 }
131 }
132
133 true
134 }
135}
136
137fn ip_in_cidr(ip: IpAddr, cidr: &str) -> bool {
139 if let Ok(net) = IpNet::from_str(cidr) {
140 return net.contains(&ip);
141 }
142 if let Ok(single) = IpAddr::from_str(cidr) {
143 return single == ip;
144 }
145 false
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 fn h(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
153 pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
154 }
155
156 fn iter(v: &[(String, String)]) -> impl IntoIterator<Item = (&str, &str)> + Clone {
157 v.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect::<Vec<_>>()
158 }
159
160 #[test]
161 fn empty_matcher_matches_everything() {
162 let m = RequestMatcher::default();
163 assert!(m.is_empty());
164 let hs = h(&[]);
165 assert!(m.matches(None, iter(&hs), None, false));
166 assert!(m.matches(Some("8.8.8.8"), iter(&hs), Some(1024), true));
167 }
168
169 #[test]
170 fn cidr_v4_match() {
171 let m = RequestMatcher {
172 source_ips: vec!["10.0.0.0/8".into()],
173 ..Default::default()
174 };
175 let hs = h(&[]);
176 assert!(m.matches(Some("10.5.6.7"), iter(&hs), None, false));
177 assert!(!m.matches(Some("11.0.0.1"), iter(&hs), None, false));
178 assert!(!m.matches(None, iter(&hs), None, false));
179 }
180
181 #[test]
182 fn bare_ip_treated_as_host() {
183 let m = RequestMatcher {
184 source_ips: vec!["127.0.0.1".into()],
185 ..Default::default()
186 };
187 let hs = h(&[]);
188 assert!(m.matches(Some("127.0.0.1"), iter(&hs), None, false));
189 assert!(!m.matches(Some("127.0.0.2"), iter(&hs), None, false));
190 }
191
192 #[test]
193 fn cidr_v6_match() {
194 let m = RequestMatcher {
195 source_ips: vec!["2001:db8::/32".into()],
196 ..Default::default()
197 };
198 let hs = h(&[]);
199 assert!(m.matches(Some("2001:db8::1"), iter(&hs), None, false));
200 assert!(!m.matches(Some("2001:db9::1"), iter(&hs), None, false));
201 }
202
203 #[test]
204 fn header_presence_only() {
205 let m = RequestMatcher {
206 headers: vec![HeaderMatch {
207 name: "x-test".into(),
208 value: None,
209 }],
210 ..Default::default()
211 };
212 let with = h(&[("x-test", "anything")]);
213 let without = h(&[("x-other", "v")]);
214 assert!(m.matches(None, iter(&with), None, false));
215 assert!(!m.matches(None, iter(&without), None, false));
216 }
217
218 #[test]
219 fn header_exact_value_case_insensitive_name() {
220 let m = RequestMatcher {
221 headers: vec![HeaderMatch {
222 name: "X-Test".into(),
223 value: Some("abc".into()),
224 }],
225 ..Default::default()
226 };
227 let good = h(&[("x-test", "abc")]);
228 let bad = h(&[("x-test", "xyz")]);
229 assert!(m.matches(None, iter(&good), None, false));
230 assert!(!m.matches(None, iter(&bad), None, false));
231 }
232
233 #[test]
234 fn body_size_threshold() {
235 let m = RequestMatcher {
236 min_body_size_bytes: Some(1024),
237 ..Default::default()
238 };
239 let hs = h(&[]);
240 assert!(m.matches(None, iter(&hs), Some(2048), false));
241 assert!(!m.matches(None, iter(&hs), Some(512), false));
242 assert!(!m.matches(None, iter(&hs), None, false));
243
244 let m2 = RequestMatcher {
245 max_body_size_bytes: Some(1024),
246 ..Default::default()
247 };
248 assert!(m2.matches(None, iter(&hs), Some(512), false));
249 assert!(!m2.matches(None, iter(&hs), Some(2048), false));
250 }
251
252 #[test]
253 fn chunked_only() {
254 let m_chunked = RequestMatcher {
255 chunked_only: Some(true),
256 ..Default::default()
257 };
258 let m_unchunked = RequestMatcher {
259 chunked_only: Some(false),
260 ..Default::default()
261 };
262 let hs = h(&[]);
263 assert!(m_chunked.matches(None, iter(&hs), None, true));
264 assert!(!m_chunked.matches(None, iter(&hs), None, false));
265 assert!(!m_unchunked.matches(None, iter(&hs), None, true));
266 assert!(m_unchunked.matches(None, iter(&hs), None, false));
267 }
268
269 #[test]
270 fn and_semantics_across_fields() {
271 let m = RequestMatcher {
272 source_ips: vec!["10.0.0.0/8".into()],
273 headers: vec![HeaderMatch {
274 name: "x-test".into(),
275 value: Some("yes".into()),
276 }],
277 min_body_size_bytes: Some(100),
278 chunked_only: Some(true),
279 ..Default::default()
280 };
281 let hs = h(&[("x-test", "yes")]);
282 assert!(m.matches(Some("10.1.1.1"), iter(&hs), Some(200), true));
283 assert!(!m.matches(Some("8.8.8.8"), iter(&hs), Some(200), true));
285 let bad_hs = h(&[("x-test", "no")]);
287 assert!(!m.matches(Some("10.1.1.1"), iter(&bad_hs), Some(200), true));
288 assert!(!m.matches(Some("10.1.1.1"), iter(&hs), Some(50), true));
290 assert!(!m.matches(Some("10.1.1.1"), iter(&hs), Some(200), false));
292 }
293
294 #[test]
295 fn invalid_cidr_does_not_panic() {
296 let m = RequestMatcher {
297 source_ips: vec!["not-an-ip".into()],
298 ..Default::default()
299 };
300 let hs = h(&[]);
301 assert!(!m.matches(Some("1.2.3.4"), iter(&hs), None, false));
302 }
303}