1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
use local_ip_address::local_ip;

use std::{
    collections::HashMap, error, ffi::OsStr, fs::File, io,
    path::PathBuf, str::FromStr,
};

#[derive(Subcommand)]
pub enum WebserverCmd {
    /// Serve
    Serve(ServeArguments)
}

#[derive(Parser,Default,Debug)]
pub struct ServeArguments {
    port: Option<usize>,
}

use clap::{Subcommand, Parser};
use tiny_http::{Header, Response, Server, StatusCode};

use crate::compile;

pub type Error = Box<dyn error::Error + Send + Sync + 'static>;

pub fn handle(cmd: WebserverCmd) {
    match cmd {
        WebserverCmd::Serve(args) => {
            _ = start_webserver(args.port);
        },
    }
}

fn start_webserver(port: Option<usize>) -> Result<(), io::Error> {
    println!("Starting webserver...");

    compile::handle(compile::CompileCmd::Repository);

    let dist_path = std::env::current_dir()
        .expect("failed to get current working directory")
        .join("dist");

    let port = port.unwrap_or(10443);

    let server = Server::http(format!("{}:{}", "0.0.0.0", port)).unwrap();

    println!("starting server at http://{}:{}", local_ip().unwrap(), port);

    for request in server.incoming_requests() {
        let mut file_path = dist_path.clone();

        if request.url().len() > 1 {
            for chunk in request.url().trim_start_matches('/').split('/') {
                file_path.push(chunk);
            }
        };
    
        println!("Requested file: {}", file_path.display());

        if file_path == dist_path {
            _ = handle_file_response(request, &dist_path.join("index").with_extension("html"));
        } else if !file_path.is_file() {
            let status = StatusCode(404);
            println!("Status: {} ({})", status.default_reason_phrase(), status.0);
            request.respond(Response::empty(status))?;
        } else {
            _ = handle_file_response(request, &file_path);
        }
    }
    unreachable!("`incoming_requests` never stops")
}

fn handle_file_response(request: tiny_http::Request, path: &PathBuf) -> Result<(), io::Error> {
    let content_type_by_extension: HashMap<&'static str, &'static str> = [
        ("html", ""),
        ("jpg", "image/jpeg"),        
        ("jpeg", "image/jpeg"),
        ("png", "image/png"),
        ("toml", "application/toml"),
        ("wasm", "application/wasm")
    ]
    .iter()
    .cloned()
    .collect();

    match File::open(&path) {
        Ok(file) => {
            let mut response = Response::from_file(file);
            let content_type = path
                .extension()
                .and_then(OsStr::to_str)
                .and_then(|ext| content_type_by_extension.get(ext).copied())
                .unwrap_or("text/plain");
                response.add_header(
                    Header::from_str(&format!("Content-Type: {}", content_type))
                        .map_err(|_| io::Error::from(io::ErrorKind::Other))?,
                );
                request.respond(response)
        },
        Err(err) => {
            let status = StatusCode(500);
            println!("Status: {} ({})", status.default_reason_phrase(), status.0);
            println!("Error: {:?}", err);
            request.respond(Response::empty(status))
        }
    }
}