agentmux_srv\backend/
docsite.rs1#![allow(dead_code)]
2use std::path::PathBuf;
13use std::sync::OnceLock;
14
15static DOCSITE_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
19
20pub fn set_docsite_dir(path: PathBuf) {
24 let _ = DOCSITE_DIR.set(if path.is_dir() { Some(path) } else { None });
25}
26
27pub fn get_docsite_dir() -> Option<&'static PathBuf> {
29 DOCSITE_DIR.get().and_then(|opt| opt.as_ref())
30}
31
32pub fn resolve_docsite_path(request_path: &str) -> Option<PathBuf> {
46 let base = get_docsite_dir()?;
47
48 let clean = request_path.trim_start_matches('/');
50 if clean.is_empty() {
51 let index = base.join("index.html");
53 if index.is_file() {
54 return Some(index);
55 }
56 return None;
57 }
58
59 if clean.contains("..") {
61 return None;
62 }
63
64 let exact = base.join(clean);
66 if exact.is_file() {
67 return Some(exact);
68 }
69
70 let with_html = base.join(format!("{}.html", clean));
72 if with_html.is_file() {
73 return Some(with_html);
74 }
75
76 None
77}
78
79pub fn is_safe_path(path: &str) -> bool {
81 !path.contains("..") && !path.contains('\0')
82}
83
84#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn test_resolve_no_docsite() {
92 assert!(resolve_docsite_path("/anything").is_none());
94 }
95
96 #[test]
97 fn test_is_safe_path() {
98 assert!(is_safe_path("docs/page"));
99 assert!(is_safe_path("index.html"));
100 assert!(!is_safe_path("../etc/passwd"));
101 assert!(!is_safe_path("docs/../../secret"));
102 assert!(!is_safe_path("path\0with\0null"));
103 }
104
105 #[test]
106 fn test_resolve_blocks_traversal() {
107 assert!(resolve_docsite_path("/../../../etc/passwd").is_none());
109 }
110}