Skip to main content

ploit_malper/
share.rs

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('&', "&amp;")
214        .replace('<', "&lt;")
215        .replace('>', "&gt;")
216        .replace('"', "&quot;")
217}