Skip to main content

xtask/
serve.rs

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}