Skip to main content

fkm_proxy/utils/
serve.rs

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    // for example: "GET / HTTP1.1"
14    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}