1use std::io::{BufRead, BufReader, Write};
2use std::net::{TcpListener, TcpStream};
3use std::path::Path;
4use std::time::Duration;
5
6pub fn run(root: &Path, port: u16) -> Result<(), String> {
7 let listener = TcpListener::bind(("127.0.0.1", port))
8 .map_err(|e| format!("bind 127.0.0.1:{port}: {e}"))?;
9 println!("serving {} at http://127.0.0.1:{port}/", root.display());
10 for stream in listener.incoming().flatten() {
11 let root = root.to_path_buf();
12 std::thread::spawn(move || respond(&root, stream));
13 }
14 Ok(())
15}
16
17fn respond(root: &Path, mut stream: TcpStream) -> std::io::Result<()> {
18 stream.set_read_timeout(Some(Duration::from_secs(5)))?;
19 let mut reader = BufReader::new(stream.try_clone()?);
20 let mut line = String::new();
21 reader.read_line(&mut line)?;
22 let target = line.split_whitespace().nth(1).unwrap_or("/");
23 let path = target.split('?').next().unwrap_or("/").to_string();
24 let mut header = String::new();
25 while reader.read_line(&mut header)? > 2 {
26 header.clear();
27 }
28 let safe = |part: &&str| !part.is_empty() && *part != ".." && !part.contains('\\');
29 let mut file = root.to_path_buf();
30 path.split('/')
31 .filter(safe)
32 .for_each(|part| file.push(part));
33 if file.is_dir() && !path.ends_with('/') {
34 let head = format!("HTTP/1.1 301 Moved Permanently\r\nLocation: {path}/\r\n");
35 return stream
36 .write_all(format!("{head}Content-Length: 0\r\nConnection: close\r\n\r\n").as_bytes());
37 }
38 if file.is_dir() {
39 file.push("index.html");
40 }
41 let (status, body) = match std::fs::read(&file) {
42 Ok(body) => ("200 OK", body),
43 Err(_) => ("404 Not Found", b"not found".to_vec()),
44 };
45 let kind = match file.extension().and_then(|e| e.to_str()) {
46 Some("html") => "text/html; charset=utf-8",
47 Some("js") => "text/javascript",
48 Some("wasm") => "application/wasm",
49 _ => "application/octet-stream",
50 };
51 let head = format!(
52 "HTTP/1.1 {status}\r\nContent-Type: {kind}\r\nContent-Length: {}\r\n",
53 body.len()
54 );
55 stream.write_all(format!("{head}Connection: close\r\n\r\n").as_bytes())?;
56 stream.write_all(&body)
57}