1use std::fs;
2use std::io::{Read, Write};
3use std::net::{TcpListener, TcpStream};
4use std::path::{Component, Path, PathBuf};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::Arc;
7use std::thread;
8use std::time::Duration;
9
10use crate::error::{AppError, Result};
11
12pub struct FileServerHandle {
13 shutdown: Arc<AtomicBool>,
14 thread: thread::JoinHandle<()>,
15 port: u16,
16}
17
18pub fn find_free_port(start_port: u16) -> Result<u16> {
19 for port in start_port..start_port.saturating_add(100) {
20 if TcpListener::bind(("0.0.0.0", port)).is_ok() {
21 return Ok(port);
22 }
23 }
24 Err(AppError::Message("No free port found in range".to_string()))
25}
26
27pub fn start_file_server(
28 directory: impl Into<PathBuf>,
29 port: u16,
30) -> Result<(FileServerHandle, u16)> {
31 let directory = directory.into();
32 let actual_port = find_free_port(port)?;
33 let listener = TcpListener::bind(("0.0.0.0", actual_port))?;
34 listener.set_nonblocking(true)?;
35
36 let shutdown = Arc::new(AtomicBool::new(false));
37 let shutdown_thread = Arc::clone(&shutdown);
38 let thread_directory = directory.clone();
39
40 let thread = thread::spawn(move || {
41 while !shutdown_thread.load(Ordering::Relaxed) {
42 match listener.accept() {
43 Ok((mut stream, _)) => {
44 let _ = handle_client(&mut stream, &thread_directory);
45 }
46 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
47 thread::sleep(Duration::from_millis(100));
48 }
49 Err(_) => break,
50 }
51 }
52 });
53
54 Ok((
55 FileServerHandle {
56 shutdown,
57 thread,
58 port: actual_port,
59 },
60 actual_port,
61 ))
62}
63
64pub fn stop_file_server(server: FileServerHandle) {
65 server.shutdown.store(true, Ordering::Relaxed);
66 let _ = TcpStream::connect(("127.0.0.1", server.port));
67 let _ = server.thread.join();
68}
69
70fn handle_client(stream: &mut TcpStream, directory: &Path) -> Result<()> {
71 let mut buffer = [0u8; 8192];
72 let size = stream.read(&mut buffer)?;
73 if size == 0 {
74 return Ok(());
75 }
76
77 let request = String::from_utf8_lossy(&buffer[..size]);
78 let mut lines = request.lines();
79 let request_line = lines.next().unwrap_or_default();
80 let mut parts = request_line.split_whitespace();
81 let method = parts.next().unwrap_or_default();
82 let path = parts.next().unwrap_or("/").split('?').next().unwrap_or("/");
83
84 if method != "GET" {
85 write_response(
86 stream,
87 405,
88 "Method Not Allowed",
89 "Only GET is supported",
90 "text/plain",
91 )?;
92 return Ok(());
93 }
94
95 if path == "/" || path.is_empty() {
96 let body = directory_listing(directory)?;
97 write_response(stream, 200, "OK", &body, "text/html; charset=utf-8")?;
98 return Ok(());
99 }
100
101 let requested = sanitize_path(directory, path);
102 match requested {
103 Some(file_path) if file_path.is_file() => {
104 let data = fs::read(&file_path)?;
105 let content_type = guess_mime(&file_path);
106 write_binary_response(stream, 200, "OK", &data, content_type)?;
107 }
108 Some(file_path) if file_path.is_dir() => {
109 let body = directory_listing(&file_path)?;
110 write_response(stream, 200, "OK", &body, "text/html; charset=utf-8")?;
111 }
112 _ => {
113 write_response(stream, 404, "Not Found", "File not found", "text/plain")?;
114 }
115 }
116
117 Ok(())
118}
119
120fn sanitize_path(base: &Path, request_path: &str) -> Option<PathBuf> {
121 let mut relative = PathBuf::new();
122 for component in Path::new(request_path).components() {
123 match component {
124 Component::Normal(part) => relative.push(part),
125 Component::CurDir | Component::RootDir => {}
126 Component::ParentDir | Component::Prefix(_) => return None,
127 }
128 }
129 Some(base.join(relative))
130}
131
132fn directory_listing(directory: &Path) -> Result<String> {
133 let mut entries = Vec::new();
134 for entry in fs::read_dir(directory)? {
135 let entry = entry?;
136 let file_name = entry.file_name().to_string_lossy().to_string();
137 let file_type = entry.file_type()?;
138 let suffix = if file_type.is_dir() { "/" } else { "" };
139 entries.push(format!(
140 "<li><a href=\"{name}{suffix}\">{name}{suffix}</a></li>",
141 name = html_escape(&file_name),
142 suffix = suffix
143 ));
144 }
145 entries.sort();
146
147 Ok(format!(
148 "<!doctype html><html><head><meta charset=\"utf-8\"><title>PloitMalper Share</title></head><body><h1>PloitMalper Share</h1><ul>{}</ul></body></html>",
149 entries.join("\n")
150 ))
151}
152
153fn guess_mime(path: &Path) -> &'static str {
154 match path
155 .extension()
156 .and_then(|ext| ext.to_str())
157 .unwrap_or_default()
158 {
159 "html" | "htm" => "text/html; charset=utf-8",
160 "md" => "text/markdown; charset=utf-8",
161 "json" => "application/json; charset=utf-8",
162 "txt" => "text/plain; charset=utf-8",
163 "png" => "image/png",
164 "jpg" | "jpeg" => "image/jpeg",
165 "gif" => "image/gif",
166 "css" => "text/css; charset=utf-8",
167 "js" => "application/javascript; charset=utf-8",
168 _ => "application/octet-stream",
169 }
170}
171
172fn write_response(
173 stream: &mut TcpStream,
174 status: u16,
175 reason: &str,
176 body: &str,
177 content_type: &str,
178) -> Result<()> {
179 let bytes = body.as_bytes();
180 let header = format!(
181 "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
182 status,
183 reason,
184 content_type,
185 bytes.len()
186 );
187 stream.write_all(header.as_bytes())?;
188 stream.write_all(bytes)?;
189 Ok(())
190}
191
192fn write_binary_response(
193 stream: &mut TcpStream,
194 status: u16,
195 reason: &str,
196 body: &[u8],
197 content_type: &str,
198) -> Result<()> {
199 let header = format!(
200 "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
201 status,
202 reason,
203 content_type,
204 body.len()
205 );
206 stream.write_all(header.as_bytes())?;
207 stream.write_all(body)?;
208 Ok(())
209}
210
211fn html_escape(value: &str) -> String {
212 value
213 .replace('&', "&")
214 .replace('<', "<")
215 .replace('>', ">")
216 .replace('"', """)
217}