1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::SystemTime;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::PolicyError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ByteSize(pub u64);
12
13impl ByteSize {
14 pub fn bytes(n: u64) -> Self {
15 ByteSize(n)
16 }
17
18 pub fn kib(n: u64) -> Self {
19 ByteSize(n * 1024)
20 }
21
22 pub fn mib(n: u64) -> Self {
23 ByteSize(n * 1024 * 1024)
24 }
25
26 pub fn gib(n: u64) -> Self {
27 ByteSize(n * 1024 * 1024 * 1024)
28 }
29
30 pub fn parse(s: &str) -> Result<Self, PolicyError> {
31 let s = s.trim();
32 if s.is_empty() {
33 return Err(PolicyError::Invalid("empty byte size string".into()));
34 }
35
36 let last = s.chars().last().unwrap();
38 if last.is_ascii_alphabetic() {
39 let (num_str, suffix) = s.split_at(s.len() - 1);
40 let n: u64 = num_str
41 .trim()
42 .parse()
43 .map_err(|_| PolicyError::Invalid(format!("invalid byte size: {}", s)))?;
44 match suffix.to_ascii_uppercase().as_str() {
45 "K" => Ok(ByteSize::kib(n)),
46 "M" => Ok(ByteSize::mib(n)),
47 "G" => Ok(ByteSize::gib(n)),
48 other => Err(PolicyError::Invalid(format!("unknown byte size suffix: {}", other))),
49 }
50 } else {
51 let n: u64 = s
52 .parse()
53 .map_err(|_| PolicyError::Invalid(format!("invalid byte size: {}", s)))?;
54 Ok(ByteSize(n))
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
61pub enum FsIsolation {
62 #[default]
63 None,
64 OverlayFs,
65 BranchFs,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
70pub enum BranchAction {
71 #[default]
72 Commit,
73 Abort,
74 Keep,
75}
76
77#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
79pub struct HttpRule {
80 pub method: String,
81 pub host: String,
82 pub path: String,
83}
84
85impl HttpRule {
86 pub fn parse(s: &str) -> Result<Self, PolicyError> {
93 let s = s.trim();
94 let (method, rest) = s
95 .split_once(char::is_whitespace)
96 .ok_or_else(|| PolicyError::Invalid(format!("invalid http rule: {}", s)))?;
97 let rest = rest.trim();
98 if rest.is_empty() {
99 return Err(PolicyError::Invalid(format!("invalid http rule: {}", s)));
100 }
101
102 let (host, path) = if let Some(pos) = rest.find('/') {
103 let (h, p) = rest.split_at(pos);
104 let has_wildcard = p.ends_with('*');
106 let mut normalized = normalize_path(p);
107 if has_wildcard && !normalized.ends_with('*') {
108 normalized.push('*');
109 }
110 (h.to_string(), normalized)
111 } else {
112 (rest.to_string(), "/*".to_string())
113 };
114
115 Ok(HttpRule {
116 method: method.to_uppercase(),
117 host,
118 path,
119 })
120 }
121
122 pub fn matches(&self, method: &str, host: &str, path: &str) -> bool {
126 if self.method != "*" && !self.method.eq_ignore_ascii_case(method) {
128 return false;
129 }
130 if self.host != "*" && !self.host.eq_ignore_ascii_case(host) {
132 return false;
133 }
134 let normalized = normalize_path(path);
136 prefix_or_exact_match(&self.path, &normalized)
137 }
138}
139
140pub fn normalize_path(path: &str) -> String {
147 let mut decoded = String::with_capacity(path.len());
149 let mut chars = path.bytes();
150 while let Some(b) = chars.next() {
151 if b == b'%' {
152 let hi = chars.next();
153 let lo = chars.next();
154 if let (Some(h), Some(l)) = (hi, lo) {
155 let hex = [h, l];
156 if let Ok(s) = std::str::from_utf8(&hex) {
157 if let Ok(val) = u8::from_str_radix(s, 16) {
158 decoded.push(val as char);
159 continue;
160 }
161 }
162 decoded.push(b as char);
164 decoded.push(h as char);
165 decoded.push(l as char);
166 } else {
167 decoded.push(b as char);
168 }
169 } else {
170 decoded.push(b as char);
171 }
172 }
173
174 let mut segments: Vec<&str> = Vec::new();
176 for seg in decoded.split('/') {
177 match seg {
178 "" | "." => {}
179 ".." => {
180 segments.pop();
181 }
182 s => segments.push(s),
183 }
184 }
185
186 let mut result = String::with_capacity(decoded.len());
188 result.push('/');
189 result.push_str(&segments.join("/"));
190 result
191}
192
193pub fn prefix_or_exact_match(pattern: &str, value: &str) -> bool {
202 if pattern == "/*" || pattern == "*" {
203 return true;
204 }
205 if let Some(prefix) = pattern.strip_suffix('*') {
206 value.starts_with(prefix)
207 } else {
208 pattern == value
209 }
210}
211
212pub fn http_acl_check(
219 allow: &[HttpRule],
220 deny: &[HttpRule],
221 method: &str,
222 host: &str,
223 path: &str,
224) -> bool {
225 for rule in deny {
227 if rule.matches(method, host, path) {
228 return false;
229 }
230 }
231 if allow.is_empty() && deny.is_empty() {
233 return true; }
235 if allow.is_empty() {
236 return true;
238 }
239 for rule in allow {
240 if rule.matches(method, host, path) {
241 return true;
242 }
243 }
244 false }
246
247#[derive(Clone, Serialize, Deserialize)]
249pub struct Policy {
250 pub fs_writable: Vec<PathBuf>,
252 pub fs_readable: Vec<PathBuf>,
253 pub fs_denied: Vec<PathBuf>,
254
255 pub deny_syscalls: Option<Vec<String>>,
257 pub allow_syscalls: Option<Vec<String>>,
258
259 pub net_allow_hosts: Option<Vec<String>>,
269 pub net_bind: Vec<u16>,
270 pub net_connect: Vec<u16>,
271 pub no_raw_sockets: bool,
272 pub no_udp: bool,
273
274 pub http_allow: Vec<HttpRule>,
276 pub http_deny: Vec<HttpRule>,
277 pub http_ports: Vec<u16>,
280 pub https_ca: Option<PathBuf>,
282 pub https_key: Option<PathBuf>,
284
285 pub max_memory: Option<ByteSize>,
289 pub max_processes: u32,
290 pub max_open_files: Option<u32>,
291 pub max_cpu: Option<u8>,
292
293 pub random_seed: Option<u64>,
295 pub time_start: Option<SystemTime>,
296 pub no_randomize_memory: bool,
297 pub no_huge_pages: bool,
298 pub no_coredump: bool,
299 pub deterministic_dirs: bool,
300 pub hostname: Option<String>,
301
302 pub fs_isolation: FsIsolation,
304 pub workdir: Option<PathBuf>,
305 pub cwd: Option<PathBuf>,
306 pub fs_storage: Option<PathBuf>,
307 pub max_disk: Option<ByteSize>,
308 pub on_exit: BranchAction,
309 pub on_error: BranchAction,
310
311 pub fs_mount: Vec<(PathBuf, PathBuf)>,
313
314 pub chroot: Option<PathBuf>,
316 pub clean_env: bool,
317 pub env: HashMap<String, String>,
318 pub gpu_devices: Option<Vec<u32>>,
320
321 pub cpu_cores: Option<Vec<u32>>,
323 pub num_cpus: Option<u32>,
324 pub port_remap: bool,
325
326 pub uid: Option<u32>,
328
329 #[serde(skip)]
331 pub policy_fn: Option<crate::policy_fn::PolicyCallback>,
332}
333
334impl std::fmt::Debug for Policy {
335 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336 f.debug_struct("Policy")
337 .field("fs_readable", &self.fs_readable)
338 .field("fs_writable", &self.fs_writable)
339 .field("max_memory", &self.max_memory)
340 .field("max_processes", &self.max_processes)
341 .field("policy_fn", &self.policy_fn.as_ref().map(|_| "<callback>"))
342 .finish_non_exhaustive()
343 }
344}
345
346impl Policy {
347 pub fn builder() -> PolicyBuilder {
348 PolicyBuilder::default()
349 }
350}
351
352#[derive(Default)]
354pub struct PolicyBuilder {
355 fs_writable: Vec<PathBuf>,
356 fs_readable: Vec<PathBuf>,
357 fs_denied: Vec<PathBuf>,
358
359 deny_syscalls: Option<Vec<String>>,
360 allow_syscalls: Option<Vec<String>>,
361
362 net_allow_hosts: Option<Vec<String>>,
363 net_bind: Vec<u16>,
364 net_connect: Vec<u16>,
365 no_raw_sockets: Option<bool>,
366 no_udp: bool,
367
368 http_allow: Vec<String>,
369 http_deny: Vec<String>,
370 http_ports: Vec<u16>,
371 https_ca: Option<PathBuf>,
372 https_key: Option<PathBuf>,
373
374 max_memory: Option<ByteSize>,
375 max_processes: Option<u32>,
376 max_open_files: Option<u32>,
377 max_cpu: Option<u8>,
378
379 random_seed: Option<u64>,
380 time_start: Option<SystemTime>,
381 no_randomize_memory: bool,
382 no_huge_pages: bool,
383 no_coredump: bool,
384 deterministic_dirs: bool,
385 hostname: Option<String>,
386
387 fs_isolation: Option<FsIsolation>,
388 workdir: Option<PathBuf>,
389 cwd: Option<PathBuf>,
390 fs_storage: Option<PathBuf>,
391 max_disk: Option<ByteSize>,
392 on_exit: Option<BranchAction>,
393 on_error: Option<BranchAction>,
394
395 fs_mount: Vec<(PathBuf, PathBuf)>,
396 chroot: Option<PathBuf>,
397 clean_env: bool,
398 env: HashMap<String, String>,
399
400 gpu_devices: Option<Vec<u32>>,
401
402 cpu_cores: Option<Vec<u32>>,
403 num_cpus: Option<u32>,
404 port_remap: bool,
405
406 uid: Option<u32>,
407 policy_fn: Option<crate::policy_fn::PolicyCallback>,
408}
409
410impl PolicyBuilder {
411 pub fn fs_write(mut self, path: impl Into<PathBuf>) -> Self {
412 self.fs_writable.push(path.into());
413 self
414 }
415
416 pub fn fs_read(mut self, path: impl Into<PathBuf>) -> Self {
417 self.fs_readable.push(path.into());
418 self
419 }
420
421 pub fn fs_read_if_exists(self, path: impl Into<PathBuf>) -> Self {
422 let path = path.into();
423 if path.exists() {
424 self.fs_read(path)
425 } else {
426 self
427 }
428 }
429
430 pub fn fs_deny(mut self, path: impl Into<PathBuf>) -> Self {
431 self.fs_denied.push(path.into());
432 self
433 }
434
435 pub fn deny_syscalls(mut self, calls: Vec<String>) -> Self {
436 self.deny_syscalls = Some(calls);
437 self
438 }
439
440 pub fn allow_syscalls(mut self, calls: Vec<String>) -> Self {
441 self.allow_syscalls = Some(calls);
442 self
443 }
444
445 pub fn net_allow_host(mut self, host: impl Into<String>) -> Self {
448 self.net_allow_hosts
449 .get_or_insert_with(Vec::new)
450 .push(host.into());
451 self
452 }
453
454 pub fn net_restrict_hosts(mut self) -> Self {
458 self.net_allow_hosts.get_or_insert_with(Vec::new);
459 self
460 }
461
462 pub fn net_bind_port(mut self, port: u16) -> Self {
463 self.net_bind.push(port);
464 self
465 }
466
467 pub fn net_connect_port(mut self, port: u16) -> Self {
468 self.net_connect.push(port);
469 self
470 }
471
472 pub fn no_raw_sockets(mut self, v: bool) -> Self {
473 self.no_raw_sockets = Some(v);
474 self
475 }
476
477 pub fn no_udp(mut self, v: bool) -> Self {
478 self.no_udp = v;
479 self
480 }
481
482 pub fn http_allow(mut self, rule: &str) -> Self {
483 self.http_allow.push(rule.to_string());
484 self
485 }
486
487 pub fn http_deny(mut self, rule: &str) -> Self {
488 self.http_deny.push(rule.to_string());
489 self
490 }
491
492 pub fn http_port(mut self, port: u16) -> Self {
493 self.http_ports.push(port);
494 if !self.net_connect.contains(&port) {
497 self.net_connect.push(port);
498 }
499 self
500 }
501
502 pub fn https_ca(mut self, path: impl Into<PathBuf>) -> Self {
503 self.https_ca = Some(path.into());
504 self
505 }
506
507 pub fn https_key(mut self, path: impl Into<PathBuf>) -> Self {
508 self.https_key = Some(path.into());
509 self
510 }
511
512 pub fn max_memory(mut self, size: ByteSize) -> Self {
513 self.max_memory = Some(size);
514 self
515 }
516
517 pub fn max_processes(mut self, n: u32) -> Self {
518 self.max_processes = Some(n);
519 self
520 }
521
522 pub fn max_open_files(mut self, n: u32) -> Self {
523 self.max_open_files = Some(n);
524 self
525 }
526
527 pub fn max_cpu(mut self, pct: u8) -> Self {
528 self.max_cpu = Some(pct);
529 self
530 }
531
532 pub fn random_seed(mut self, seed: u64) -> Self {
533 self.random_seed = Some(seed);
534 self
535 }
536
537 pub fn time_start(mut self, t: SystemTime) -> Self {
538 self.time_start = Some(t);
539 self
540 }
541
542 pub fn no_randomize_memory(mut self, v: bool) -> Self {
543 self.no_randomize_memory = v;
544 self
545 }
546
547 pub fn no_huge_pages(mut self, v: bool) -> Self {
548 self.no_huge_pages = v;
549 self
550 }
551
552 pub fn no_coredump(mut self, v: bool) -> Self {
553 self.no_coredump = v;
554 self
555 }
556
557 pub fn deterministic_dirs(mut self, v: bool) -> Self {
558 self.deterministic_dirs = v;
559 self
560 }
561
562 pub fn hostname(mut self, name: impl Into<String>) -> Self {
563 self.hostname = Some(name.into());
564 self
565 }
566
567 pub fn fs_isolation(mut self, iso: FsIsolation) -> Self {
568 self.fs_isolation = Some(iso);
569 self
570 }
571
572 pub fn workdir(mut self, path: impl Into<PathBuf>) -> Self {
573 self.workdir = Some(path.into());
574 self
575 }
576
577 pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
578 self.cwd = Some(path.into());
579 self
580 }
581
582 pub fn fs_storage(mut self, path: impl Into<PathBuf>) -> Self {
583 self.fs_storage = Some(path.into());
584 self
585 }
586
587 pub fn max_disk(mut self, size: ByteSize) -> Self {
588 self.max_disk = Some(size);
589 self
590 }
591
592 pub fn on_exit(mut self, action: BranchAction) -> Self {
593 self.on_exit = Some(action);
594 self
595 }
596
597 pub fn on_error(mut self, action: BranchAction) -> Self {
598 self.on_error = Some(action);
599 self
600 }
601
602 pub fn chroot(mut self, path: impl Into<PathBuf>) -> Self {
603 self.chroot = Some(path.into());
604 self
605 }
606
607 pub fn fs_mount(mut self, virtual_path: impl Into<PathBuf>, host_path: impl Into<PathBuf>) -> Self {
608 self.fs_mount.push((virtual_path.into(), host_path.into()));
609 self
610 }
611
612 pub fn clean_env(mut self, v: bool) -> Self {
613 self.clean_env = v;
614 self
615 }
616
617 pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
618 self.env.insert(key.into(), value.into());
619 self
620 }
621
622
623 pub fn gpu_devices(mut self, devices: Vec<u32>) -> Self {
624 self.gpu_devices = Some(devices);
625 self
626 }
627
628 pub fn cpu_cores(mut self, cores: Vec<u32>) -> Self {
629 self.cpu_cores = Some(cores);
630 self
631 }
632
633 pub fn num_cpus(mut self, n: u32) -> Self {
634 self.num_cpus = Some(n);
635 self
636 }
637
638 pub fn port_remap(mut self, v: bool) -> Self {
639 self.port_remap = v;
640 self
641 }
642
643 pub fn policy_fn(
644 mut self,
645 f: impl Fn(crate::policy_fn::SyscallEvent, &mut crate::policy_fn::PolicyContext) -> crate::policy_fn::Verdict + Send + Sync + 'static,
646 ) -> Self {
647 self.policy_fn = Some(std::sync::Arc::new(f));
648 self
649 }
650
651 pub fn uid(mut self, id: u32) -> Self {
652 self.uid = Some(id);
653 self
654 }
655
656 pub fn build(self) -> Result<Policy, PolicyError> {
657 if self.deny_syscalls.is_some() && self.allow_syscalls.is_some() {
659 return Err(PolicyError::MutuallyExclusiveSyscalls);
660 }
661
662 if let Some(cpu) = self.max_cpu {
664 if cpu == 0 || cpu > 100 {
665 return Err(PolicyError::InvalidCpuPercent(cpu));
666 }
667 }
668
669 if self.https_ca.is_some() != self.https_key.is_some() {
671 return Err(PolicyError::Invalid(
672 "--https-ca and --https-key must both be provided together".into(),
673 ));
674 }
675
676 let http_allow: Vec<HttpRule> = self
678 .http_allow
679 .iter()
680 .map(|s| HttpRule::parse(s))
681 .collect::<Result<_, _>>()?;
682 let http_deny: Vec<HttpRule> = self
683 .http_deny
684 .iter()
685 .map(|s| HttpRule::parse(s))
686 .collect::<Result<_, _>>()?;
687
688 let http_ports = if self.http_ports.is_empty() && (!http_allow.is_empty() || !http_deny.is_empty()) {
690 let mut ports = vec![80];
691 if self.https_ca.is_some() {
692 ports.push(443);
693 }
694 ports
695 } else {
696 self.http_ports
697 };
698
699 let fs_isolation = self.fs_isolation.unwrap_or_default();
701 if fs_isolation != FsIsolation::None && self.workdir.is_none() {
702 return Err(PolicyError::FsIsolationRequiresWorkdir);
703 }
704
705 Ok(Policy {
706 fs_writable: self.fs_writable,
707 fs_readable: self.fs_readable,
708 fs_denied: self.fs_denied,
709 deny_syscalls: self.deny_syscalls,
710 allow_syscalls: self.allow_syscalls,
711 net_allow_hosts: self.net_allow_hosts,
712 net_bind: self.net_bind,
713 net_connect: self.net_connect,
714 no_raw_sockets: self.no_raw_sockets.unwrap_or(true),
715 no_udp: self.no_udp,
716 http_allow,
717 http_deny,
718 http_ports,
719 https_ca: self.https_ca,
720 https_key: self.https_key,
721 max_memory: self.max_memory,
722 max_processes: self.max_processes.unwrap_or(64),
723 max_open_files: self.max_open_files,
724 max_cpu: self.max_cpu,
725 random_seed: self.random_seed,
726 time_start: self.time_start,
727 no_randomize_memory: self.no_randomize_memory,
728 no_huge_pages: self.no_huge_pages,
729 no_coredump: self.no_coredump,
730 deterministic_dirs: self.deterministic_dirs,
731 hostname: self.hostname,
732 fs_isolation,
733 workdir: self.workdir,
734 cwd: self.cwd,
735 fs_storage: self.fs_storage,
736 max_disk: self.max_disk,
737 on_exit: self.on_exit.unwrap_or_default(),
738 on_error: self.on_error.unwrap_or_default(),
739 fs_mount: self.fs_mount,
740 chroot: self.chroot,
741 clean_env: self.clean_env,
742 env: self.env,
743 gpu_devices: self.gpu_devices,
744 cpu_cores: self.cpu_cores,
745 num_cpus: self.num_cpus,
746 port_remap: self.port_remap,
747 uid: self.uid,
748 policy_fn: self.policy_fn,
749 })
750 }
751}
752
753#[cfg(test)]
754mod http_rule_tests {
755 use super::*;
756
757 #[test]
760 fn parse_basic_get() {
761 let rule = HttpRule::parse("GET api.example.com/v1/*").unwrap();
762 assert_eq!(rule.method, "GET");
763 assert_eq!(rule.host, "api.example.com");
764 assert_eq!(rule.path, "/v1/*");
765 }
766
767 #[test]
768 fn parse_wildcard_method_and_host() {
769 let rule = HttpRule::parse("* */admin/*").unwrap();
770 assert_eq!(rule.method, "*");
771 assert_eq!(rule.host, "*");
772 assert_eq!(rule.path, "/admin/*");
773 }
774
775 #[test]
776 fn parse_post_with_exact_path() {
777 let rule = HttpRule::parse("POST example.com/upload").unwrap();
778 assert_eq!(rule.method, "POST");
779 assert_eq!(rule.host, "example.com");
780 assert_eq!(rule.path, "/upload");
781 }
782
783 #[test]
784 fn parse_no_path_defaults_to_wildcard() {
785 let rule = HttpRule::parse("GET example.com").unwrap();
786 assert_eq!(rule.method, "GET");
787 assert_eq!(rule.host, "example.com");
788 assert_eq!(rule.path, "/*");
789 }
790
791 #[test]
792 fn parse_method_uppercased() {
793 let rule = HttpRule::parse("get example.com/foo").unwrap();
794 assert_eq!(rule.method, "GET");
795 }
796
797 #[test]
798 fn parse_error_no_space() {
799 assert!(HttpRule::parse("GETexample.com").is_err());
800 }
801
802 #[test]
803 fn parse_error_empty_host() {
804 assert!(HttpRule::parse("GET ").is_err());
805 }
806
807 #[test]
810 fn prefix_or_exact_match_wildcard_all() {
811 assert!(prefix_or_exact_match("/*", "/anything"));
812 assert!(prefix_or_exact_match("*", "/anything"));
813 assert!(prefix_or_exact_match("/*", "/"));
814 }
815
816 #[test]
817 fn prefix_or_exact_match_prefix() {
818 assert!(prefix_or_exact_match("/v1/*", "/v1/foo"));
819 assert!(prefix_or_exact_match("/v1/*", "/v1/foo/bar"));
820 assert!(prefix_or_exact_match("/v1/*", "/v1/"));
821 assert!(!prefix_or_exact_match("/v1/*", "/v2/foo"));
822 }
823
824 #[test]
825 fn prefix_or_exact_match_exact() {
826 assert!(prefix_or_exact_match("/v1/models", "/v1/models"));
827 assert!(!prefix_or_exact_match("/v1/models", "/v1/models/extra"));
828 assert!(!prefix_or_exact_match("/v1/models", "/v1/model"));
829 }
830
831 #[test]
834 fn matches_exact() {
835 let rule = HttpRule::parse("GET api.example.com/v1/models").unwrap();
836 assert!(rule.matches("GET", "api.example.com", "/v1/models"));
837 assert!(!rule.matches("POST", "api.example.com", "/v1/models"));
838 assert!(!rule.matches("GET", "other.com", "/v1/models"));
839 assert!(!rule.matches("GET", "api.example.com", "/v1/other"));
840 }
841
842 #[test]
843 fn matches_wildcard_method() {
844 let rule = HttpRule::parse("* api.example.com/v1/*").unwrap();
845 assert!(rule.matches("GET", "api.example.com", "/v1/foo"));
846 assert!(rule.matches("POST", "api.example.com", "/v1/bar"));
847 }
848
849 #[test]
850 fn matches_wildcard_host() {
851 let rule = HttpRule::parse("GET */v1/*").unwrap();
852 assert!(rule.matches("GET", "any.host.com", "/v1/foo"));
853 }
854
855 #[test]
856 fn matches_case_insensitive_method() {
857 let rule = HttpRule::parse("GET example.com/foo").unwrap();
858 assert!(rule.matches("get", "example.com", "/foo"));
859 assert!(rule.matches("Get", "example.com", "/foo"));
860 }
861
862 #[test]
863 fn matches_case_insensitive_host() {
864 let rule = HttpRule::parse("GET Example.COM/foo").unwrap();
865 assert!(rule.matches("GET", "example.com", "/foo"));
866 }
867
868 #[test]
871 fn acl_no_rules_allows_all() {
872 assert!(http_acl_check(&[], &[], "GET", "example.com", "/foo"));
873 }
874
875 #[test]
876 fn acl_allow_only_permits_matching() {
877 let allow = vec![HttpRule::parse("GET api.example.com/v1/*").unwrap()];
878 assert!(http_acl_check(&allow, &[], "GET", "api.example.com", "/v1/foo"));
879 assert!(!http_acl_check(&allow, &[], "POST", "api.example.com", "/v1/foo"));
880 assert!(!http_acl_check(&allow, &[], "GET", "other.com", "/v1/foo"));
881 }
882
883 #[test]
884 fn acl_deny_only_blocks_matching() {
885 let deny = vec![HttpRule::parse("* */admin/*").unwrap()];
886 assert!(!http_acl_check(&[], &deny, "GET", "example.com", "/admin/settings"));
887 assert!(http_acl_check(&[], &deny, "GET", "example.com", "/public/page"));
888 }
889
890 #[test]
891 fn acl_deny_takes_precedence_over_allow() {
892 let allow = vec![HttpRule::parse("* example.com/*").unwrap()];
893 let deny = vec![HttpRule::parse("* example.com/admin/*").unwrap()];
894 assert!(http_acl_check(&allow, &deny, "GET", "example.com", "/public"));
895 assert!(!http_acl_check(&allow, &deny, "GET", "example.com", "/admin/settings"));
896 }
897
898 #[test]
899 fn acl_allow_deny_by_default_when_no_match() {
900 let allow = vec![HttpRule::parse("GET api.example.com/v1/*").unwrap()];
901 assert!(!http_acl_check(&allow, &[], "GET", "evil.com", "/v1/foo"));
903 }
904
905 #[test]
908 fn builder_http_rules() {
909 let policy = Policy::builder()
910 .http_allow("GET api.example.com/v1/*")
911 .http_deny("* */admin/*")
912 .build()
913 .unwrap();
914 assert_eq!(policy.http_allow.len(), 1);
915 assert_eq!(policy.http_deny.len(), 1);
916 assert_eq!(policy.http_allow[0].method, "GET");
917 assert_eq!(policy.http_deny[0].host, "*");
918 }
919
920 #[test]
921 fn builder_invalid_http_allow_returns_error() {
922 let result = Policy::builder()
923 .http_allow("GETexample.com")
924 .build();
925 assert!(result.is_err());
926 }
927
928 #[test]
929 fn builder_invalid_http_deny_returns_error() {
930 let result = Policy::builder()
931 .http_deny("BADRULE")
932 .build();
933 assert!(result.is_err());
934 }
935
936 #[test]
937 fn builder_https_ca_without_key_returns_error() {
938 let result = Policy::builder()
939 .https_ca("/tmp/ca.pem")
940 .build();
941 assert!(result.is_err());
942 }
943
944 #[test]
945 fn builder_https_key_without_ca_returns_error() {
946 let result = Policy::builder()
947 .https_key("/tmp/key.pem")
948 .build();
949 assert!(result.is_err());
950 }
951
952 #[test]
953 fn builder_https_ca_and_key_together_ok() {
954 let policy = Policy::builder()
955 .https_ca("/tmp/ca.pem")
956 .https_key("/tmp/key.pem")
957 .build()
958 .unwrap();
959 assert!(policy.https_ca.is_some());
960 assert!(policy.https_key.is_some());
961 }
962
963 #[test]
966 fn normalize_path_basic() {
967 assert_eq!(normalize_path("/foo/bar"), "/foo/bar");
968 assert_eq!(normalize_path("/"), "/");
969 }
970
971 #[test]
972 fn normalize_path_double_slashes() {
973 assert_eq!(normalize_path("/foo//bar"), "/foo/bar");
974 assert_eq!(normalize_path("//foo///bar//"), "/foo/bar");
975 }
976
977 #[test]
978 fn normalize_path_dot_segments() {
979 assert_eq!(normalize_path("/foo/./bar"), "/foo/bar");
980 assert_eq!(normalize_path("/foo/../bar"), "/bar");
981 assert_eq!(normalize_path("/foo/bar/../../baz"), "/baz");
982 }
983
984 #[test]
985 fn normalize_path_dotdot_at_root() {
986 assert_eq!(normalize_path("/../foo"), "/foo");
987 assert_eq!(normalize_path("/../../foo"), "/foo");
988 }
989
990 #[test]
991 fn normalize_path_percent_encoding() {
992 assert_eq!(normalize_path("/foo%2Fbar"), "/foo/bar");
994 assert_eq!(normalize_path("/%61dmin/settings"), "/admin/settings");
995 }
996
997 #[test]
998 fn normalize_path_mixed_bypass_attempts() {
999 assert_eq!(normalize_path("/v1/./admin/settings"), "/v1/admin/settings");
1001 assert_eq!(normalize_path("/v1/../admin/settings"), "/admin/settings");
1002 assert_eq!(normalize_path("/v1//admin/settings"), "/v1/admin/settings");
1003 assert_eq!(normalize_path("/v1/%2e%2e/admin"), "/admin");
1004 }
1005
1006 #[test]
1009 fn acl_deny_prevents_double_slash_bypass() {
1010 let deny = vec![HttpRule::parse("* */admin/*").unwrap()];
1011 assert!(!http_acl_check(&[], &deny, "GET", "example.com", "/admin/settings"));
1013 assert!(!http_acl_check(&[], &deny, "GET", "example.com", "//admin/settings"));
1014 assert!(!http_acl_check(&[], &deny, "GET", "example.com", "/admin//settings"));
1015 }
1016
1017 #[test]
1018 fn acl_deny_prevents_dot_segment_bypass() {
1019 let deny = vec![HttpRule::parse("* */admin/*").unwrap()];
1020 assert!(!http_acl_check(&[], &deny, "GET", "example.com", "/./admin/settings"));
1021 assert!(!http_acl_check(&[], &deny, "GET", "example.com", "/public/../admin/settings"));
1022 }
1023
1024 #[test]
1025 fn acl_deny_prevents_percent_encoding_bypass() {
1026 let deny = vec![HttpRule::parse("* */admin/*").unwrap()];
1027 assert!(!http_acl_check(&[], &deny, "GET", "example.com", "/%61dmin/settings"));
1029 }
1030
1031 #[test]
1032 fn acl_allow_normalized_path_still_works() {
1033 let allow = vec![HttpRule::parse("GET example.com/v1/models").unwrap()];
1034 assert!(http_acl_check(&allow, &[], "GET", "example.com", "/v1/models"));
1035 assert!(http_acl_check(&allow, &[], "GET", "example.com", "/v1/./models"));
1036 assert!(http_acl_check(&allow, &[], "GET", "example.com", "/v1//models"));
1037 assert!(!http_acl_check(&allow, &[], "GET", "example.com", "/v1/models/extra"));
1039 assert!(!http_acl_check(&allow, &[], "GET", "example.com", "/v2/models"));
1040 }
1041
1042 #[test]
1043 fn parse_normalizes_rule_path() {
1044 let rule = HttpRule::parse("GET example.com/v1/./models/*").unwrap();
1045 assert_eq!(rule.path, "/v1/models/*");
1046
1047 let rule = HttpRule::parse("GET example.com/v1//models").unwrap();
1048 assert_eq!(rule.path, "/v1/models");
1049 }
1050}