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 ipv4_embedding_ipv6_denied_by_default() {
185 let probes = [
188 "::7f00:1", "::a00:1", "::808:808", "64:ff9b::7f00:1", "64:ff9b::a9fe:a9fe", "2002:7f00:1::", "2002:c0a8:101::1", "::ffff:0:7f00:1", "::ffff:7f00:1", "64:ff9b:1::7f00:1", ];
199
200 let acl = HttpAclBuilder::new()
201 .ip_acl_default(true)
202 .try_build()
203 .unwrap();
204 for probe in probes {
205 assert!(
206 acl.is_ip_allowed(&probe.parse().unwrap()).is_denied(),
207 "{probe} should be denied"
208 );
209 }
210 assert!(
211 acl.is_ip_allowed(&"2606:4700:4700::1111".parse().unwrap())
212 .is_allowed()
213 );
214
215 let acl = HttpAclBuilder::new()
216 .non_global_ip_ranges(true)
217 .ip_acl_default(true)
218 .try_build()
219 .unwrap();
220 for probe in probes {
221 assert!(
222 acl.is_ip_allowed(&probe.parse().unwrap()).is_allowed(),
223 "{probe} should be allowed with non-global ranges on"
224 );
225 }
226
227 for range in [
228 "::7f00:0/104",
229 "64:ff9b::/96",
230 "2002::/16",
231 "::ffff:0:0:0/96",
232 ] {
233 assert!(
234 HttpAclBuilder::new()
235 .add_allowed_ip_range(range.parse::<IpNet>().unwrap())
236 .unwrap()
237 .try_build()
238 .is_err(),
239 "{range} should be refused as an allowed range"
240 );
241 }
242 }
243
244 #[test]
245 fn non_global_ip_acl() {
246 let acl = HttpAclBuilder::new()
247 .non_global_ip_ranges(true)
248 .ip_acl_default(true)
249 .try_build()
250 .unwrap();
251
252 assert!(
253 acl.is_ip_allowed(&"192.168.1.1".parse().unwrap())
254 .is_allowed()
255 );
256 assert!(
257 acl.is_ip_allowed(&"203.0.113.12".parse().unwrap())
258 .is_allowed()
259 );
260
261 let acl = HttpAclBuilder::new()
262 .ip_acl_default(true)
263 .try_build()
264 .unwrap();
265
266 assert!(
267 acl.is_ip_allowed(&"192.168.1.1".parse().unwrap())
268 .is_denied()
269 );
270 assert!(
271 acl.is_ip_allowed(&"203.0.113.12".parse().unwrap())
272 .is_denied()
273 );
274 }
275
276 #[test]
277 fn default_ip_acl() {
278 let acl = HttpAclBuilder::new().try_build().unwrap();
279
280 assert!(
281 acl.is_ip_allowed(&"192.168.1.1".parse().unwrap())
282 .is_denied()
283 );
284 assert!(acl.is_ip_allowed(&"1.1.1.1".parse().unwrap()).is_denied());
285 assert!(!acl.is_port_allowed(8080).is_allowed());
286 }
287
288 #[test]
289 fn url_path_acl() {
290 let acl = HttpAclBuilder::new()
291 .add_allowed_url_path("/allowed".to_string())
292 .unwrap()
293 .add_allowed_url_path("/allowed/:id".to_string())
294 .unwrap()
295 .add_denied_url_path("/denied".to_string())
296 .unwrap()
297 .add_denied_url_path("/denied/{*path}".to_string())
298 .unwrap()
299 .try_build()
300 .unwrap();
301
302 assert!(acl.is_url_path_allowed("/allowed").is_allowed());
303 assert!(acl.is_url_path_allowed("/allowed/allowed").is_allowed());
304 assert!(acl.is_url_path_allowed("/denied").is_denied());
305 assert!(acl.is_url_path_allowed("/denied/denied").is_denied());
306 assert!(acl.is_url_path_allowed("/denied/denied/denied").is_denied());
307 }
308
309 #[test]
310 fn header_acl() {
311 let acl = HttpAclBuilder::new()
312 .add_allowed_header("X-Allowed".to_string(), Some("true".to_string()))
313 .unwrap()
314 .add_allowed_header("X-Allowed2".to_string(), None)
315 .unwrap()
316 .add_denied_header("X-Denied".to_string(), Some("true".to_string()))
317 .unwrap()
318 .add_denied_header("X-Denied2".to_string(), None)
319 .unwrap()
320 .try_build()
321 .unwrap();
322
323 assert!(acl.is_header_allowed("X-Allowed", "true").is_allowed());
324 assert!(acl.is_header_allowed("X-Allowed2", "false").is_allowed());
325 assert!(acl.is_header_allowed("X-Denied", "true").is_denied());
326 assert!(acl.is_header_allowed("X-Denied2", "false").is_denied());
327 }
328
329 #[test]
330 fn header_acl_bytes_compares_raw_bytes() {
331 let acl = HttpAclBuilder::new()
335 .add_allowed_header("x-allowed".to_string(), Some("caf\u{FFFD}".to_string()))
336 .unwrap()
337 .add_allowed_header("x-any".to_string(), None)
338 .unwrap()
339 .add_denied_header("x-denied".to_string(), None)
340 .unwrap()
341 .add_denied_header("x-denied-value".to_string(), Some("caf\u{e9}".to_string()))
342 .unwrap()
343 .header_acl_default(false)
344 .try_build()
345 .unwrap();
346
347 assert!(
348 acl.is_header_allowed_bytes("x-allowed", "caf\u{FFFD}".as_bytes())
349 .is_allowed()
350 );
351 assert!(
352 acl.is_header_allowed_bytes("x-allowed", b"caf\xE9")
353 .is_denied()
354 );
355 assert!(
356 acl.is_header_allowed_bytes("x-any", b"caf\xE9")
357 .is_allowed()
358 );
359 assert!(
360 acl.is_header_allowed_bytes("x-denied", b"caf\xE9")
361 .is_denied()
362 );
363 assert!(
364 acl.is_header_allowed_bytes("x-denied-value", "caf\u{e9}".as_bytes())
365 .is_denied()
366 );
367 assert!(
368 acl.is_header_allowed_bytes("x-denied-value", b"caf\xE9")
369 .is_allowed()
370 );
371 assert!(
372 acl.is_header_allowed_bytes("x-other", b"caf\xE9")
373 .is_denied()
374 );
375 }
376
377 #[test]
378 fn static_dns_mapping() {
379 let regular_addr = "10.0.0.1:80".parse().unwrap();
380 let trusted_addr = "10.0.0.2:80".parse().unwrap();
381
382 let acl = HttpAclBuilder::new()
383 .add_static_dns_mapping("regular.example.com".to_string(), regular_addr)
384 .unwrap()
385 .add_trusted_static_dns_mapping("trusted.example.com".to_string(), trusted_addr)
386 .unwrap()
387 .try_build()
388 .unwrap();
389
390 assert_eq!(
391 acl.resolve_static_dns_mapping("regular.example.com"),
392 Some(regular_addr)
393 );
394 assert_eq!(
395 acl.resolve_trusted_static_dns_mapping("trusted.example.com"),
396 Some(trusted_addr)
397 );
398 assert_eq!(
400 acl.resolve_trusted_static_dns_mapping("regular.example.com"),
401 None
402 );
403 assert_eq!(acl.resolve_static_dns_mapping("trusted.example.com"), None);
404
405 assert!(acl.is_ip_allowed(®ular_addr.ip()).is_denied());
408
409 assert!(
411 HttpAclBuilder::new()
412 .add_static_dns_mapping("both.example.com".to_string(), regular_addr)
413 .unwrap()
414 .add_trusted_static_dns_mapping("both.example.com".to_string(), trusted_addr)
415 .is_err()
416 );
417 assert!(
418 HttpAclBuilder::new()
419 .add_trusted_static_dns_mapping("both.example.com".to_string(), trusted_addr)
420 .unwrap()
421 .add_static_dns_mapping("both.example.com".to_string(), regular_addr)
422 .is_err()
423 );
424 }
425
426 #[test]
427 fn valid_acl() {
428 let acl = HttpAclBuilder::new()
429 .try_build_full(HttpAclHooks {
430 validate_fn: Some(Arc::new(|scheme, authority, headers, body| {
431 if scheme == "http" {
432 return AclClassification::DeniedUserAcl;
433 }
434
435 if authority.host.is_ip() {
436 return AclClassification::DeniedUserAcl;
437 }
438
439 for (header_name, header_value) in headers {
440 if header_name == "<dangerous-header>"
441 && header_value == "<dangerous-value>"
442 {
443 return AclClassification::DeniedUserAcl;
444 }
445 }
446
447 if let Some(body) = body
448 && body == b"<dangerous-body>"
449 {
450 return AclClassification::DeniedUserAcl;
451 }
452
453 AclClassification::AllowedDefault
454 })),
455 ..Default::default()
456 })
457 .unwrap();
458
459 assert!(
460 acl.is_valid(
461 "https",
462 &"example.com".into(),
463 [("<header>", "<value>")].into_iter(),
464 Some(b"body"),
465 )
466 .is_allowed()
467 );
468 assert!(
469 acl.is_valid(
470 "http",
471 &"example.com".into(),
472 [("<header>", "<value>")].into_iter(),
473 Some(b"body"),
474 )
475 .is_denied()
476 );
477 assert!(
478 acl.is_valid(
479 "https",
480 &"1.1.1.1".parse::<IpAddr>().unwrap().into(),
481 [("<header>", "<value>")].into_iter(),
482 Some(b"body"),
483 )
484 .is_denied()
485 );
486 assert!(
487 acl.is_valid(
488 "https",
489 &"example.com".into(),
490 [("<dangerous-header>", "<dangerous-value>")].into_iter(),
491 Some(b"body"),
492 )
493 .is_denied()
494 );
495 assert!(
496 acl.is_valid(
497 "https",
498 &"example.com".into(),
499 [("<header>", "<value>")].into_iter(),
500 Some(b"<dangerous-body>"),
501 )
502 .is_denied()
503 );
504 }
505
506 #[test]
507 fn modify_hooks_unset_by_default() {
508 let acl = HttpAclBuilder::new().build();
509
510 assert!(!acl.has_modify_request());
511 assert!(!acl.has_modify_response());
512
513 let mut request_mutation = super::RequestMutation::default();
514 acl.modify_request("https", &"example.com".into(), &mut request_mutation);
515 assert_eq!(request_mutation, super::RequestMutation::default());
516
517 let mut response_mutation = super::ResponseMutation {
518 status: 200,
519 headers: vec![("content-type".to_string(), "text/plain".to_string())],
520 body: bytes::Bytes::from_static(b"body"),
521 };
522 let unchanged = response_mutation.clone();
523 acl.modify_response("https", &"example.com".into(), &mut response_mutation);
524 assert_eq!(response_mutation, unchanged);
525 }
526
527 #[test]
528 fn modify_hooks_invoked_with_expected_context() {
529 let acl = HttpAclBuilder::new().build_full(HttpAclHooks {
530 modify_request_fn: Some(std::sync::Arc::new(|scheme, authority, mutation| {
531 mutation
532 .headers
533 .push(("x-injected".to_string(), format!("{scheme}://{authority}")));
534 mutation.body = Some(bytes::Bytes::from_static(b"replaced"));
535 })),
536 modify_response_fn: Some(std::sync::Arc::new(|scheme, authority, mutation| {
537 mutation.status = 201;
538 mutation.headers.retain(|(k, _)| k != "x-remove-me");
539 mutation.body = bytes::Bytes::from(format!("{scheme}://{authority}"));
540 })),
541 ..Default::default()
542 });
543
544 assert!(acl.has_modify_request());
545 assert!(acl.has_modify_response());
546
547 let mut request_mutation = super::RequestMutation::default();
548 acl.modify_request("https", &"example.com".into(), &mut request_mutation);
549 assert_eq!(
550 request_mutation.headers,
551 vec![("x-injected".to_string(), "https://example.com".to_string())]
552 );
553 assert_eq!(
554 request_mutation.body,
555 Some(bytes::Bytes::from_static(b"replaced"))
556 );
557
558 let mut response_mutation = super::ResponseMutation {
559 status: 200,
560 headers: vec![
561 ("x-remove-me".to_string(), "yes".to_string()),
562 ("x-keep-me".to_string(), "yes".to_string()),
563 ],
564 body: bytes::Bytes::new(),
565 };
566 acl.modify_response("https", &"example.com".into(), &mut response_mutation);
567 assert_eq!(response_mutation.status, 201);
568 assert_eq!(
569 response_mutation.headers,
570 vec![("x-keep-me".to_string(), "yes".to_string())]
571 );
572 assert_eq!(response_mutation.body, "https://example.com");
573 }
574}