1use crate::error::{Error, Result};
4use crate::files::{serve_file, FileOptions};
5use crate::handler::Handler;
6use crate::request::Request;
7use crate::response::Response;
8use async_trait::async_trait;
9use std::path::PathBuf;
10use tokio::fs;
11
12pub struct StaticFileHandler {
18 root_dir: PathBuf,
19}
20
21impl StaticFileHandler {
22 pub fn new(root_dir: &str) -> Self {
23 Self {
24 root_dir: PathBuf::from(root_dir),
25 }
26 }
27
28 async fn serve(&self, req: &Request, file_path: &str) -> Result<Response> {
29 let not_found = || Error::not_found("File not found");
30
31 let decoded = urlencoding::decode(file_path).map_err(|_| not_found())?;
33 let mut relative = PathBuf::new();
34 for segment in decoded.split('/').filter(|s| !s.is_empty()) {
35 let unsafe_segment = segment.starts_with('.')
36 || segment.contains(['\\', '\0', ':'])
37 || segment.chars().any(char::is_control);
38 if unsafe_segment {
39 return Err(not_found());
40 }
41 relative.push(segment);
42 }
43
44 let canonical_root = fs::canonicalize(&self.root_dir)
45 .await
46 .map_err(|_| Error::not_found("Static directory not found"))?;
47 let mut canonical = fs::canonicalize(canonical_root.join(&relative))
48 .await
49 .map_err(|_| not_found())?;
50 if !canonical.starts_with(&canonical_root) {
52 return Err(not_found());
53 }
54
55 let meta = fs::metadata(&canonical).await.map_err(|_| not_found())?;
56 if meta.is_dir() {
57 let index = canonical.join("index.html");
58 if fs::metadata(&index).await.is_ok_and(|m| m.is_file()) {
59 canonical = index;
60 } else {
61 return Err(Error::forbidden("Directory listing not allowed"));
62 }
63 }
64
65 serve_file(
66 req,
67 &canonical,
68 &FileOptions::new().cache_control("public, max-age=3600"),
69 )
70 .await
71 }
72}
73
74#[async_trait]
75impl Handler for StaticFileHandler {
76 async fn call(&self, req: Request) -> Result<Response> {
77 let filepath = req.params.get("filepath").unwrap_or("").to_string();
78 self.serve(&req, &filepath).await
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use crate::files::content_type_for;
85 use std::path::Path;
86
87 #[test]
88 fn test_content_type_detection() {
89 assert_eq!(
90 content_type_for(Path::new("test.html")),
91 "text/html; charset=utf-8"
92 );
93 assert_eq!(
94 content_type_for(Path::new("test.css")),
95 "text/css; charset=utf-8"
96 );
97 assert_eq!(
98 content_type_for(Path::new("test.js")),
99 "application/javascript; charset=utf-8"
100 );
101 assert_eq!(content_type_for(Path::new("test.json")), "application/json");
102 assert_eq!(content_type_for(Path::new("test.png")), "image/png");
103 assert_eq!(
104 content_type_for(Path::new("unknown")),
105 "application/octet-stream"
106 );
107 }
108}