1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4pub use ipnet::IpNet;
5
6pub mod acl;
7pub mod error;
8pub mod mutation;
9pub mod utils;
10
11pub use acl::{
12 AclClassification, HttpAcl, HttpAclBuilder, HttpAclHooks, HttpRequestMethod, ValidateFn,
13};
14pub use mutation::{ModifyRequestFn, ModifyResponseFn, RequestMutation, ResponseMutation};
15pub use utils::IntoIpRange;
16
17#[cfg(test)]
18mod tests {
19 use std::net::IpAddr;
20 use std::sync::Arc;
21
22 use ipnet::IpNet;
23
24 use super::{AclClassification, HttpAclBuilder, HttpAclHooks};
25
26 #[test]
27 fn acl() {
28 let acl = HttpAclBuilder::new()
29 .add_allowed_host("example.com".to_string())
30 .unwrap()
31 .add_allowed_host("example.org".to_string())
32 .unwrap()
33 .add_denied_host("example.net".to_string())
34 .unwrap()
35 .add_allowed_port_range(8080..=8080)
36 .unwrap()
37 .add_denied_port_range(8443..=8443)
38 .unwrap()
39 .add_allowed_ip_range("1.0.0.0/8".parse::<IpNet>().unwrap())
40 .unwrap()
41 .add_denied_ip_range("9.0.0.0/8".parse::<IpNet>().unwrap())
42 .unwrap()
43 .try_build()
44 .unwrap();
45
46 assert!(acl.is_host_allowed("example.com").is_allowed());
47 assert!(acl.is_host_allowed("example.org").is_allowed());
48 assert!(!acl.is_host_allowed("example.net").is_allowed());
49 assert!(acl.is_port_allowed(8080).is_allowed());
50 assert!(!acl.is_port_allowed(8443).is_allowed());
51 assert!(acl.is_ip_allowed(&"1.1.1.1".parse().unwrap()).is_allowed());
52 assert!(acl.is_ip_allowed(&"9.9.9.9".parse().unwrap()).is_denied());
53 assert!(
54 acl.is_ip_allowed(&"192.168.1.1".parse().unwrap())
55 .is_denied()
56 );
57 }
58
59 #[test]
60 fn host_acl() {
61 let acl = HttpAclBuilder::new()
62 .add_allowed_host("example.com".to_string())
63 .unwrap()
64 .add_allowed_host("example.org".to_string())
65 .unwrap()
66 .add_denied_host("example.net".to_string())
67 .unwrap()
68 .try_build()
69 .unwrap();
70
71 assert!(acl.is_host_allowed("example.com").is_allowed());
72 assert!(acl.is_host_allowed("example.org").is_allowed());
73 assert!(!acl.is_host_allowed("example.net").is_allowed());
74 }
75
76 #[test]
77 fn wildcard_host_acl() {
78 let acl = HttpAclBuilder::new()
79 .add_allowed_host("*.example.com".to_string())
80 .unwrap()
81 .add_allowed_host("?.example.org".to_string())
82 .unwrap()
83 .add_denied_host("secret.example.com".to_string())
84 .unwrap()
85 .try_build()
86 .unwrap();
87
88 assert!(acl.is_host_allowed("foo.example.com").is_allowed());
90 assert!(acl.is_host_allowed("foo.bar.example.com").is_allowed());
91 assert!(!acl.is_host_allowed("example.com").is_allowed());
92
93 assert!(acl.is_host_allowed("foo.example.org").is_allowed());
95 assert!(!acl.is_host_allowed("foo.bar.example.org").is_allowed());
96 assert!(!acl.is_host_allowed("example.org").is_allowed());
97
98 assert!(acl.is_host_allowed("secret.example.com").is_allowed());
102
103 assert!(acl.is_host_allowed("example.net").is_denied());
105 }
106
107 #[test]
108 fn invalid_wildcard_host_pattern_rejected() {
109 assert!(
110 HttpAclBuilder::new()
111 .add_allowed_host("foo*.example.com".to_string())
112 .is_err()
113 );
114 }
115
116 #[test]
117 fn port_acl() {
118 let acl = HttpAclBuilder::new()
119 .clear_allowed_port_ranges()
120 .add_allowed_port_range(8080..=8080)
121 .unwrap()
122 .add_denied_port_range(8441..=8443)
123 .unwrap()
124 .try_build()
125 .unwrap();
126
127 assert!(acl.is_port_allowed(80).is_denied());
128 assert!(acl.is_port_allowed(8080).is_allowed());
129 assert!(acl.is_port_allowed(8440).is_denied());
130 assert!(!acl.is_port_allowed(8441).is_allowed());
131 assert!(!acl.is_port_allowed(8442).is_allowed());
132 assert!(!acl.is_port_allowed(8443).is_allowed());
133 assert!(acl.is_port_allowed(8444).is_denied());
134 }
135
136 #[test]
137 fn denied_port_ranges_setter_rejects_overlaps() {
138 assert!(
140 HttpAclBuilder::new()
141 .denied_port_ranges(vec![70..=85])
142 .is_err()
143 );
144
145 assert!(
147 HttpAclBuilder::new()
148 .denied_port_ranges(vec![1000..=2000, 1500..=2500])
149 .is_err()
150 );
151 }
152
153 #[test]
154 fn mixed_family_ip_range_rejected() {
155 let start: IpAddr = "1.0.0.0".parse().unwrap();
156 let end: IpAddr = "::1".parse().unwrap();
157 assert!(
158 HttpAclBuilder::new()
159 .add_allowed_ip_range((start, end))
160 .is_err()
161 );
162 }
163
164 #[test]
165 fn ip_acl() {
166 let acl = HttpAclBuilder::new()
167 .clear_allowed_ip_ranges()
168 .add_allowed_ip_range("1.0.0.0/8".parse::<IpNet>().unwrap())
169 .unwrap()
170 .add_denied_ip_range("9.0.0.0/8".parse::<IpNet>().unwrap())
171 .unwrap()
172 .try_build()
173 .unwrap();
174
175 assert!(acl.is_ip_allowed(&"1.1.1.1".parse().unwrap()).is_allowed());
176 assert!(acl.is_ip_allowed(&"9.9.9.9".parse().unwrap()).is_denied());
177 assert!(
178 acl.is_ip_allowed(&"192.168.1.1".parse().unwrap())
179 .is_denied()
180 );
181 }
182
183 #[test]
184 fn non_global_ip_acl() {
185 let acl = HttpAclBuilder::new()
186 .non_global_ip_ranges(true)
187 .ip_acl_default(true)
188 .try_build()
189 .unwrap();
190
191 assert!(
192 acl.is_ip_allowed(&"192.168.1.1".parse().unwrap())
193 .is_allowed()
194 );
195 assert!(
196 acl.is_ip_allowed(&"203.0.113.12".parse().unwrap())
197 .is_allowed()
198 );
199
200 let acl = HttpAclBuilder::new()
201 .ip_acl_default(true)
202 .try_build()
203 .unwrap();
204
205 assert!(
206 acl.is_ip_allowed(&"192.168.1.1".parse().unwrap())
207 .is_denied()
208 );
209 assert!(
210 acl.is_ip_allowed(&"203.0.113.12".parse().unwrap())
211 .is_denied()
212 );
213 }
214
215 #[test]
216 fn default_ip_acl() {
217 let acl = HttpAclBuilder::new().try_build().unwrap();
218
219 assert!(
220 acl.is_ip_allowed(&"192.168.1.1".parse().unwrap())
221 .is_denied()
222 );
223 assert!(acl.is_ip_allowed(&"1.1.1.1".parse().unwrap()).is_denied());
224 assert!(!acl.is_port_allowed(8080).is_allowed());
225 }
226
227 #[test]
228 fn url_path_acl() {
229 let acl = HttpAclBuilder::new()
230 .add_allowed_url_path("/allowed".to_string())
231 .unwrap()
232 .add_allowed_url_path("/allowed/:id".to_string())
233 .unwrap()
234 .add_denied_url_path("/denied".to_string())
235 .unwrap()
236 .add_denied_url_path("/denied/{*path}".to_string())
237 .unwrap()
238 .try_build()
239 .unwrap();
240
241 assert!(acl.is_url_path_allowed("/allowed").is_allowed());
242 assert!(acl.is_url_path_allowed("/allowed/allowed").is_allowed());
243 assert!(acl.is_url_path_allowed("/denied").is_denied());
244 assert!(acl.is_url_path_allowed("/denied/denied").is_denied());
245 assert!(acl.is_url_path_allowed("/denied/denied/denied").is_denied());
246 }
247
248 #[test]
249 fn header_acl() {
250 let acl = HttpAclBuilder::new()
251 .add_allowed_header("X-Allowed".to_string(), Some("true".to_string()))
252 .unwrap()
253 .add_allowed_header("X-Allowed2".to_string(), None)
254 .unwrap()
255 .add_denied_header("X-Denied".to_string(), Some("true".to_string()))
256 .unwrap()
257 .add_denied_header("X-Denied2".to_string(), None)
258 .unwrap()
259 .try_build()
260 .unwrap();
261
262 assert!(acl.is_header_allowed("X-Allowed", "true").is_allowed());
263 assert!(acl.is_header_allowed("X-Allowed2", "false").is_allowed());
264 assert!(acl.is_header_allowed("X-Denied", "true").is_denied());
265 assert!(acl.is_header_allowed("X-Denied2", "false").is_denied());
266 }
267
268 #[test]
269 fn static_dns_mapping() {
270 let regular_addr = "10.0.0.1:80".parse().unwrap();
271 let trusted_addr = "10.0.0.2:80".parse().unwrap();
272
273 let acl = HttpAclBuilder::new()
274 .add_static_dns_mapping("regular.example.com".to_string(), regular_addr)
275 .unwrap()
276 .add_trusted_static_dns_mapping("trusted.example.com".to_string(), trusted_addr)
277 .unwrap()
278 .try_build()
279 .unwrap();
280
281 assert_eq!(
282 acl.resolve_static_dns_mapping("regular.example.com"),
283 Some(regular_addr)
284 );
285 assert_eq!(
286 acl.resolve_trusted_static_dns_mapping("trusted.example.com"),
287 Some(trusted_addr)
288 );
289 assert_eq!(
291 acl.resolve_trusted_static_dns_mapping("regular.example.com"),
292 None
293 );
294 assert_eq!(acl.resolve_static_dns_mapping("trusted.example.com"), None);
295
296 assert!(acl.is_ip_allowed(®ular_addr.ip()).is_denied());
299
300 assert!(
302 HttpAclBuilder::new()
303 .add_static_dns_mapping("both.example.com".to_string(), regular_addr)
304 .unwrap()
305 .add_trusted_static_dns_mapping("both.example.com".to_string(), trusted_addr)
306 .is_err()
307 );
308 assert!(
309 HttpAclBuilder::new()
310 .add_trusted_static_dns_mapping("both.example.com".to_string(), trusted_addr)
311 .unwrap()
312 .add_static_dns_mapping("both.example.com".to_string(), regular_addr)
313 .is_err()
314 );
315 }
316
317 #[test]
318 fn valid_acl() {
319 let acl = HttpAclBuilder::new()
320 .try_build_full(HttpAclHooks {
321 validate_fn: Some(Arc::new(|scheme, authority, headers, body| {
322 if scheme == "http" {
323 return AclClassification::DeniedUserAcl;
324 }
325
326 if authority.host.is_ip() {
327 return AclClassification::DeniedUserAcl;
328 }
329
330 for (header_name, header_value) in headers {
331 if header_name == "<dangerous-header>"
332 && header_value == "<dangerous-value>"
333 {
334 return AclClassification::DeniedUserAcl;
335 }
336 }
337
338 if let Some(body) = body
339 && body == b"<dangerous-body>"
340 {
341 return AclClassification::DeniedUserAcl;
342 }
343
344 AclClassification::AllowedDefault
345 })),
346 ..Default::default()
347 })
348 .unwrap();
349
350 assert!(
351 acl.is_valid(
352 "https",
353 &"example.com".into(),
354 [("<header>", "<value>")].into_iter(),
355 Some(b"body"),
356 )
357 .is_allowed()
358 );
359 assert!(
360 acl.is_valid(
361 "http",
362 &"example.com".into(),
363 [("<header>", "<value>")].into_iter(),
364 Some(b"body"),
365 )
366 .is_denied()
367 );
368 assert!(
369 acl.is_valid(
370 "https",
371 &"1.1.1.1".parse::<IpAddr>().unwrap().into(),
372 [("<header>", "<value>")].into_iter(),
373 Some(b"body"),
374 )
375 .is_denied()
376 );
377 assert!(
378 acl.is_valid(
379 "https",
380 &"example.com".into(),
381 [("<dangerous-header>", "<dangerous-value>")].into_iter(),
382 Some(b"body"),
383 )
384 .is_denied()
385 );
386 assert!(
387 acl.is_valid(
388 "https",
389 &"example.com".into(),
390 [("<header>", "<value>")].into_iter(),
391 Some(b"<dangerous-body>"),
392 )
393 .is_denied()
394 );
395 }
396
397 #[test]
398 fn modify_hooks_unset_by_default() {
399 let acl = HttpAclBuilder::new().build();
400
401 assert!(!acl.has_modify_request());
402 assert!(!acl.has_modify_response());
403
404 let mut request_mutation = super::RequestMutation::default();
405 acl.modify_request("https", &"example.com".into(), &mut request_mutation);
406 assert_eq!(request_mutation, super::RequestMutation::default());
407
408 let mut response_mutation = super::ResponseMutation {
409 status: 200,
410 headers: vec![("content-type".to_string(), "text/plain".to_string())],
411 body: bytes::Bytes::from_static(b"body"),
412 };
413 let unchanged = response_mutation.clone();
414 acl.modify_response("https", &"example.com".into(), &mut response_mutation);
415 assert_eq!(response_mutation, unchanged);
416 }
417
418 #[test]
419 fn modify_hooks_invoked_with_expected_context() {
420 let acl = HttpAclBuilder::new().build_full(HttpAclHooks {
421 modify_request_fn: Some(std::sync::Arc::new(|scheme, authority, mutation| {
422 mutation
423 .headers
424 .push(("x-injected".to_string(), format!("{scheme}://{authority}")));
425 mutation.body = Some(bytes::Bytes::from_static(b"replaced"));
426 })),
427 modify_response_fn: Some(std::sync::Arc::new(|scheme, authority, mutation| {
428 mutation.status = 201;
429 mutation.headers.retain(|(k, _)| k != "x-remove-me");
430 mutation.body = bytes::Bytes::from(format!("{scheme}://{authority}"));
431 })),
432 ..Default::default()
433 });
434
435 assert!(acl.has_modify_request());
436 assert!(acl.has_modify_response());
437
438 let mut request_mutation = super::RequestMutation::default();
439 acl.modify_request("https", &"example.com".into(), &mut request_mutation);
440 assert_eq!(
441 request_mutation.headers,
442 vec![("x-injected".to_string(), "https://example.com".to_string())]
443 );
444 assert_eq!(
445 request_mutation.body,
446 Some(bytes::Bytes::from_static(b"replaced"))
447 );
448
449 let mut response_mutation = super::ResponseMutation {
450 status: 200,
451 headers: vec![
452 ("x-remove-me".to_string(), "yes".to_string()),
453 ("x-keep-me".to_string(), "yes".to_string()),
454 ],
455 body: bytes::Bytes::new(),
456 };
457 acl.modify_response("https", &"example.com".into(), &mut response_mutation);
458 assert_eq!(response_mutation.status, 201);
459 assert_eq!(
460 response_mutation.headers,
461 vec![("x-keep-me".to_string(), "yes".to_string())]
462 );
463 assert_eq!(response_mutation.body, "https://example.com");
464 }
465}