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