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 header_acl_bytes_compares_raw_bytes() {
270 let acl = HttpAclBuilder::new()
274 .add_allowed_header("x-allowed".to_string(), Some("caf\u{FFFD}".to_string()))
275 .unwrap()
276 .add_allowed_header("x-any".to_string(), None)
277 .unwrap()
278 .add_denied_header("x-denied".to_string(), None)
279 .unwrap()
280 .add_denied_header("x-denied-value".to_string(), Some("caf\u{e9}".to_string()))
281 .unwrap()
282 .header_acl_default(false)
283 .try_build()
284 .unwrap();
285
286 assert!(
287 acl.is_header_allowed_bytes("x-allowed", "caf\u{FFFD}".as_bytes())
288 .is_allowed()
289 );
290 assert!(
291 acl.is_header_allowed_bytes("x-allowed", b"caf\xE9")
292 .is_denied()
293 );
294 assert!(
295 acl.is_header_allowed_bytes("x-any", b"caf\xE9")
296 .is_allowed()
297 );
298 assert!(
299 acl.is_header_allowed_bytes("x-denied", b"caf\xE9")
300 .is_denied()
301 );
302 assert!(
303 acl.is_header_allowed_bytes("x-denied-value", "caf\u{e9}".as_bytes())
304 .is_denied()
305 );
306 assert!(
307 acl.is_header_allowed_bytes("x-denied-value", b"caf\xE9")
308 .is_allowed()
309 );
310 assert!(
311 acl.is_header_allowed_bytes("x-other", b"caf\xE9")
312 .is_denied()
313 );
314 }
315
316 #[test]
317 fn static_dns_mapping() {
318 let regular_addr = "10.0.0.1:80".parse().unwrap();
319 let trusted_addr = "10.0.0.2:80".parse().unwrap();
320
321 let acl = HttpAclBuilder::new()
322 .add_static_dns_mapping("regular.example.com".to_string(), regular_addr)
323 .unwrap()
324 .add_trusted_static_dns_mapping("trusted.example.com".to_string(), trusted_addr)
325 .unwrap()
326 .try_build()
327 .unwrap();
328
329 assert_eq!(
330 acl.resolve_static_dns_mapping("regular.example.com"),
331 Some(regular_addr)
332 );
333 assert_eq!(
334 acl.resolve_trusted_static_dns_mapping("trusted.example.com"),
335 Some(trusted_addr)
336 );
337 assert_eq!(
339 acl.resolve_trusted_static_dns_mapping("regular.example.com"),
340 None
341 );
342 assert_eq!(acl.resolve_static_dns_mapping("trusted.example.com"), None);
343
344 assert!(acl.is_ip_allowed(®ular_addr.ip()).is_denied());
347
348 assert!(
350 HttpAclBuilder::new()
351 .add_static_dns_mapping("both.example.com".to_string(), regular_addr)
352 .unwrap()
353 .add_trusted_static_dns_mapping("both.example.com".to_string(), trusted_addr)
354 .is_err()
355 );
356 assert!(
357 HttpAclBuilder::new()
358 .add_trusted_static_dns_mapping("both.example.com".to_string(), trusted_addr)
359 .unwrap()
360 .add_static_dns_mapping("both.example.com".to_string(), regular_addr)
361 .is_err()
362 );
363 }
364
365 #[test]
366 fn valid_acl() {
367 let acl = HttpAclBuilder::new()
368 .try_build_full(HttpAclHooks {
369 validate_fn: Some(Arc::new(|scheme, authority, headers, body| {
370 if scheme == "http" {
371 return AclClassification::DeniedUserAcl;
372 }
373
374 if authority.host.is_ip() {
375 return AclClassification::DeniedUserAcl;
376 }
377
378 for (header_name, header_value) in headers {
379 if header_name == "<dangerous-header>"
380 && header_value == "<dangerous-value>"
381 {
382 return AclClassification::DeniedUserAcl;
383 }
384 }
385
386 if let Some(body) = body
387 && body == b"<dangerous-body>"
388 {
389 return AclClassification::DeniedUserAcl;
390 }
391
392 AclClassification::AllowedDefault
393 })),
394 ..Default::default()
395 })
396 .unwrap();
397
398 assert!(
399 acl.is_valid(
400 "https",
401 &"example.com".into(),
402 [("<header>", "<value>")].into_iter(),
403 Some(b"body"),
404 )
405 .is_allowed()
406 );
407 assert!(
408 acl.is_valid(
409 "http",
410 &"example.com".into(),
411 [("<header>", "<value>")].into_iter(),
412 Some(b"body"),
413 )
414 .is_denied()
415 );
416 assert!(
417 acl.is_valid(
418 "https",
419 &"1.1.1.1".parse::<IpAddr>().unwrap().into(),
420 [("<header>", "<value>")].into_iter(),
421 Some(b"body"),
422 )
423 .is_denied()
424 );
425 assert!(
426 acl.is_valid(
427 "https",
428 &"example.com".into(),
429 [("<dangerous-header>", "<dangerous-value>")].into_iter(),
430 Some(b"body"),
431 )
432 .is_denied()
433 );
434 assert!(
435 acl.is_valid(
436 "https",
437 &"example.com".into(),
438 [("<header>", "<value>")].into_iter(),
439 Some(b"<dangerous-body>"),
440 )
441 .is_denied()
442 );
443 }
444
445 #[test]
446 fn modify_hooks_unset_by_default() {
447 let acl = HttpAclBuilder::new().build();
448
449 assert!(!acl.has_modify_request());
450 assert!(!acl.has_modify_response());
451
452 let mut request_mutation = super::RequestMutation::default();
453 acl.modify_request("https", &"example.com".into(), &mut request_mutation);
454 assert_eq!(request_mutation, super::RequestMutation::default());
455
456 let mut response_mutation = super::ResponseMutation {
457 status: 200,
458 headers: vec![("content-type".to_string(), "text/plain".to_string())],
459 body: bytes::Bytes::from_static(b"body"),
460 };
461 let unchanged = response_mutation.clone();
462 acl.modify_response("https", &"example.com".into(), &mut response_mutation);
463 assert_eq!(response_mutation, unchanged);
464 }
465
466 #[test]
467 fn modify_hooks_invoked_with_expected_context() {
468 let acl = HttpAclBuilder::new().build_full(HttpAclHooks {
469 modify_request_fn: Some(std::sync::Arc::new(|scheme, authority, mutation| {
470 mutation
471 .headers
472 .push(("x-injected".to_string(), format!("{scheme}://{authority}")));
473 mutation.body = Some(bytes::Bytes::from_static(b"replaced"));
474 })),
475 modify_response_fn: Some(std::sync::Arc::new(|scheme, authority, mutation| {
476 mutation.status = 201;
477 mutation.headers.retain(|(k, _)| k != "x-remove-me");
478 mutation.body = bytes::Bytes::from(format!("{scheme}://{authority}"));
479 })),
480 ..Default::default()
481 });
482
483 assert!(acl.has_modify_request());
484 assert!(acl.has_modify_response());
485
486 let mut request_mutation = super::RequestMutation::default();
487 acl.modify_request("https", &"example.com".into(), &mut request_mutation);
488 assert_eq!(
489 request_mutation.headers,
490 vec![("x-injected".to_string(), "https://example.com".to_string())]
491 );
492 assert_eq!(
493 request_mutation.body,
494 Some(bytes::Bytes::from_static(b"replaced"))
495 );
496
497 let mut response_mutation = super::ResponseMutation {
498 status: 200,
499 headers: vec![
500 ("x-remove-me".to_string(), "yes".to_string()),
501 ("x-keep-me".to_string(), "yes".to_string()),
502 ],
503 body: bytes::Bytes::new(),
504 };
505 acl.modify_response("https", &"example.com".into(), &mut response_mutation);
506 assert_eq!(response_mutation.status, 201);
507 assert_eq!(
508 response_mutation.headers,
509 vec![("x-keep-me".to_string(), "yes".to_string())]
510 );
511 assert_eq!(response_mutation.body, "https://example.com");
512 }
513}