Skip to main content

http_acl/
lib.rs

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        // `*` matches one or more labels.
89        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        // `?` matches exactly one label.
94        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        // Allow-list entries (including wildcard ones) are checked before deny-list
99        // entries, same as for exact hosts - a broad wildcard allow can shadow a more
100        // specific deny, so `secret.example.com` here is allowed, not denied.
101        assert!(acl.is_host_allowed("secret.example.com").is_allowed());
102
103        // Unrelated hosts fall through to the default (deny).
104        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        // Overlaps (but doesn't exactly equal) the default allowed range `80..=80`.
139        assert!(
140            HttpAclBuilder::new()
141                .denied_port_ranges(vec![70..=85])
142                .is_err()
143        );
144
145        // Overlaps within the new list itself.
146        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        // The two maps are looked up independently - a host in one isn't found in the other.
290        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        // A regular mapping's resolved address is still subject to the IP/port ACL
297        // (denied here because no IP ranges are allowed by default).
298        assert!(acl.is_ip_allowed(&regular_addr.ip()).is_denied());
299
300        // A host can't be present in both maps.
301        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}