1use crate::utils::{
2 client::Consts,
3 http::{construct_raw_http_resp, write_http_resp},
4};
5use anyhow::Result;
6use std::path::{Path, PathBuf};
7use tokio::io::{AsyncReadExt, AsyncWriteExt};
8
9pub async fn serve_files<S>(stream: &mut S, files_index: bool, consts: &Consts) -> Result<()>
10where
11 S: AsyncReadExt + AsyncWriteExt + Unpin,
12{
13 let mut buffer = [0u8; 1];
15 let mut parts = String::new();
16 loop {
17 stream.read_exact(&mut buffer).await?;
18 if buffer[0] == 0x0A {
19 break;
20 }
21 parts.push(buffer[0] as char);
22 }
23
24 let parts = parts.trim().split(" ").collect::<Vec<&str>>();
25 let path = parts[1].trim_start_matches('/').trim_end_matches('/');
26 let base = std::env::current_dir().unwrap_or(PathBuf::from("/tmp"));
27
28 let local_path = base.join(path);
29 let canonical = match local_path.canonicalize() {
30 Ok(p) => p,
31 Err(_) => return Err(anyhow::anyhow!("File not found")),
32 };
33 let canonical_base = base.canonicalize().unwrap_or(base.clone());
34
35 if !canonical.starts_with(&canonical_base) {
36 return Err(anyhow::anyhow!("Access denied"));
37 }
38
39 if local_path.exists() {
40 if let Ok(metadata) = local_path.metadata() {
41 if metadata.is_dir() {
42 let index_file = local_path.join("index.html");
43 if index_file.exists() {
44 serve_file(stream, &index_file, consts).await?;
45 } else if files_index {
46 let mut generated = String::new();
47 let mut directories = Vec::new();
48 let mut files = Vec::new();
49
50 if let Ok(dir) = local_path.read_dir() {
51 for entry in dir.flatten() {
52 if let Ok(metadata) = entry.metadata() {
53 let filename = entry.file_name();
54 let filename = filename.to_str().unwrap_or("---").to_string();
55 let file_path = if path.is_empty() {
56 format!("/{filename}")
57 } else {
58 format!("/{path}/{filename}")
59 };
60
61 let last_modified = metadata
62 .modified()
63 .map(|t| {
64 let datetime: chrono::DateTime<chrono::Local> = t.into();
65 datetime.format("%Y-%m-%d %H:%M:%S").to_string()
66 })
67 .unwrap_or("---".to_string());
68
69 let file_size = if metadata.is_file() {
70 let size = metadata.len();
71 if size < 1024 {
72 format!("{size} B")
73 } else if size < 1024 * 1024 {
74 format!("{:.1} KB", size as f64 / 1024.0)
75 } else {
76 format!("{:.1} MB", size as f64 / (1024.0 * 1024.0))
77 }
78 } else {
79 "-".to_string()
80 };
81
82 let entry_data = (file_path, filename, last_modified, file_size);
83 if metadata.is_dir() {
84 directories.push(entry_data);
85 } else if metadata.is_file() {
86 files.push(entry_data);
87 }
88 }
89 }
90 }
91
92 directories.sort_by(|a, b| a.1.cmp(&b.1));
93 files.sort_by(|a, b| a.1.cmp(&b.1));
94
95 let parent_path = if let Some(parent) = Path::new(&path).parent() {
96 &format!("/{}", parent.to_str().unwrap_or(""))
97 } else {
98 "/"
99 };
100
101 generated += &format!(
102 "<tr>\n <td><a href=\"{}\">..</a></td>\n <td></td>\n <td></td>\n</tr>\n",
103 html_escape::encode_text(parent_path),
104 );
105
106 for (file_path, filename, last_modified, file_size) in directories {
107 generated += &format!(
108 "<tr>\n <td><a href=\"{}\">{}</a></td>\n <td>{}</td>\n <td>{}</td>\n</tr>\n",
109 html_escape::encode_text(&file_path),
110 html_escape::encode_text(&filename),
111 html_escape::encode_text(&last_modified),
112 html_escape::encode_text(&file_size)
113 );
114 }
115 for (file_path, filename, last_modified, file_size) in files {
116 generated += &format!(
117 "<tr>\n <td><a href=\"{}\">{}</a></td>\n <td>{}</td>\n <td>{}</td>\n</tr>\n",
118 html_escape::encode_text(&file_path),
119 html_escape::encode_text(&filename),
120 html_escape::encode_text(&last_modified),
121 html_escape::encode_text(&file_size)
122 );
123 }
124
125 write_http_resp(
126 stream,
127 200,
128 &consts
129 .list_html
130 .replace("{CONTENT}", &generated)
131 .replace("{DIR_PATH}", &format!("/{path}")),
132 "text/html",
133 )
134 .await?;
135 } else {
136 write_http_resp(
137 stream,
138 404,
139 &consts.error_html.replace("{MSG}", "Local file not found!"),
140 "text/html",
141 )
142 .await?;
143 }
144 } else if metadata.is_file() {
145 serve_file(stream, &local_path, consts).await?;
146 }
147 } else {
148 write_http_resp(
149 stream,
150 500,
151 &consts
152 .error_html
153 .replace("{MSG}", "File metadata not found!"),
154 "text/html",
155 )
156 .await?;
157 }
158 } else {
159 write_http_resp(
160 stream,
161 404,
162 &consts.error_html.replace("{MSG}", "Local file not found!"),
163 "text/html",
164 )
165 .await?;
166 }
167
168 Ok(())
169}
170
171async fn serve_file<S>(stream: &mut S, path: &PathBuf, consts: &Consts) -> Result<()>
172where
173 S: AsyncReadExt + AsyncWriteExt + Unpin,
174{
175 let file_contents = tokio::fs::read(path).await;
176 if let Ok(content) = file_contents {
177 let resp = construct_raw_http_resp(
178 200,
179 &content,
180 mime_guess::from_path(path)
181 .first_raw()
182 .unwrap_or("text/plain"),
183 );
184
185 stream.write_all(&resp).await?;
186 } else {
187 write_http_resp(
188 stream,
189 500,
190 &consts.error_html.replace("{MSG}", "Local file read error!"),
191 "text/html",
192 )
193 .await?;
194 }
195
196 Ok(())
197}