pharos_runtime_static/
navigation.rs1use pharos_runtime_core::http::HttpRequest;
4
5pub fn mount_redirect(request: &HttpRequest, mount: &str) -> Option<Vec<u8>> {
7 if !matches!(request.method.as_str(), "GET" | "HEAD")
8 || !mount.starts_with('/')
9 || mount.starts_with("//")
10 || mount.len() > 1024
11 || !mount
12 .bytes()
13 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'/' | b'_' | b'-' | b'.'))
14 || request.target.bytes().any(|byte| byte.is_ascii_control())
15 {
16 return None;
17 }
18 let mount = mount.trim_end_matches('/');
19 let (path, query) = request
20 .target
21 .split_once('?')
22 .map_or((request.target.as_str(), None), |(path, query)| {
23 (path, Some(query))
24 });
25 if mount.is_empty() || path != mount {
26 return None;
27 }
28 let location = query.map_or_else(|| format!("{mount}/"), |query| format!("{mount}/?{query}"));
29 Some(format!("HTTP/1.1 308 Permanent Redirect\r\nLocation: {location}\r\nContent-Length: 0\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n").into_bytes())
30}
31
32#[cfg(test)]
33mod tests {
34 use super::*;
35
36 #[test]
38 fn canonicalizes_only_bare_navigation_mounts() {
39 let mut request = HttpRequest {
40 method: "GET".into(),
41 target: "/dev_docs?search=database".into(),
42 ..Default::default()
43 };
44 let response = String::from_utf8(mount_redirect(&request, "/dev_docs").unwrap()).unwrap();
45 assert!(response.contains("Location: /dev_docs/?search=database\r\n"));
46 request.target = "/dev_docs/".into();
47 assert!(mount_redirect(&request, "/dev_docs").is_none());
48 request.target = "/dev_docs".into();
49 request.method = "POST".into();
50 assert!(mount_redirect(&request, "/dev_docs").is_none());
51 request.method = "GET".into();
52 request.target = "/dev_docs?x=\r\nInjected: true".into();
53 assert!(mount_redirect(&request, "/dev_docs").is_none());
54 }
55}