use std::io::{Write, BufReader, BufRead}; use std::thread; use std::net::{TcpListener, TcpStream}; use std::fs; use std::string::{String}; use std::process::Command; fn process_cgi(filename: String) -> String { // This is gonna be the boggiest implementation of CGI that anyone // has ever seen in the history of the fucking world let query; let script; if filename.contains("?") { let vars = filename.to_string().find("?").unwrap(); let vars1 = vars + 1; query = &filename[vars1..]; script = &filename[..vars]; } else { query = ""; script = &filename; } let output = Command::new(script) .arg("GATEWAY_INTERFACE=\"CGI/1.1\"") .arg("SERVER_SOFTWARE=\"Herb/0.3.0\"") .arg("REQUEST_METHOD=\"GET\"") .arg(format!("QUERY_STRING=\"{}\"", query)) .arg(format!("SCRIPT_NAME=\"{}\"", script)) .output() .expect("failed to execute process"); let outputd = String::from_utf8_lossy(&output.stdout).to_string(); return outputd; } fn detect_media_type(filename: String) -> String { // The Lynx terminal browser made me do this. let path = filename.to_string(); let paths = check_if_path_exists(path); let path = filename.to_string(); let dir = check_if_dir(path); if paths == true && dir == false && filename.contains(".") == false { return "text/html".to_string(); } else { let ext_index = filename.to_string().find(".").unwrap(); let test = ext_index + 1; let ext = &filename[test..]; if filename[test..].contains("cgi") { return "text/html".to_string(); } match ext { "aac" => "audio/aac".to_string(), "avi" => "video/x-msvideo".to_string(), "bmp" => "image/bmp".to_string(), "bz2" => "application/x-bzip2".to_string(), "css" => "text/css".to_string(), "gz" => "application/gzip".to_string(), "gif" => "image/gif".to_string(), "png" => "image/png".to_string(), "pdf" => "application/pdf".to_string(), "jpeg" => "image/jpeg".to_string(), "jpg" => "image/jpeg".to_string(), "js" => "text/javascript".to_string(), "mid" => "audio/midi".to_string(), "midi" => "audio/midi".to_string(), "mp3" => "audio/mpeg".to_string(), "mp4" => "video/mp4".to_string(), "mpeg" => "video/mpeg".to_string(), "ogg" => "audio/ogg".to_string(), "oga" => "audio/ogg".to_string(), "ogv" => "video/ogg".to_string(), "opus" => "audio/opus".to_string(), "txt" => "text/plain".to_string(), "html" => "text/html".to_string(), "htm" => "text/html".to_string(), "cgi" => "text/html".to_string(), ".error_server_404" => "text/html".to_string(), ".error_server_503" => "text/html".to_string(), _ => "application/octet-stream".to_string(), } } } fn check_if_path_exists(path: String) -> bool { // This is probably not the best way of checking if a path // exists but I don't care :) let file = fs::metadata(path); match file { Ok(_) => true, Err(_) => false, } } fn check_if_dir(directory: String) -> bool { let path = directory + "/index.html"; let result = check_if_path_exists(path); return result; } fn generate_index(directory: String) -> String { let mut index = format!("

Directory of {}

", directory); for file in fs::read_dir(directory).unwrap() { index = format!("{}
{:#?}", index, file.as_ref().unwrap().path().display(), file.unwrap().file_name()); } return index.to_string(); } fn get_page(filename: String) -> String { // The loaded page should be left immutable as it does // not need to be modified by the server. let path = filename.to_string(); let checks = check_if_path_exists(path); let path = filename.to_string(); let index = check_if_dir(path); let path = filename.to_string(); println!("{} {} {}", path, checks, index); if checks == true && index == false { if path.contains(".") != true && path.contains(".cgi?") != true { let result = generate_index(filename); return result.to_string(); } else if path.contains(".cgi?") { let result = process_cgi(filename); return result.to_string(); } } if filename == ".error_server_404" { let result = "

404 Not Found

The resource you are trying to locate cannot be accessed!

"; return result.to_string(); } else if filename == ".error_server_503" { let result = "

503 Not Implemented

The request sent by your web browser cannot be handled by this server software.

"; return result.to_string(); } else if filename.contains(".cgi") { let result = process_cgi(filename); return result.to_string(); } else { let result = fs::read_to_string(filename); match result { Ok(i) => i.to_string(), Err(_e) => "

403 Forbidden

The resource you are trying to access cannot be read by the server.

".to_string(), } } } fn grab_time() -> String{ let output = Command::new("date") .arg("+'%a, %d %b %Y %H:%m:%S %Z'") .output() .expect("failed to execute process"); return String::from_utf8_lossy(&output.stdout).to_string(); } fn process_request(request: Vec) -> String { let mut input = String::from_utf8_lossy(&request).to_string(); let debug = false; let mut index = String::new(); let output; if input.contains("GET") { // To-do: find a less atrocious way to do this. println!("Stream sent GET request."); if debug { println!("{}", input); } input = input.replace("GET ", ""); input = input.replace(" HTTP/1.1\r\n", ""); // Lynx also made me do this input = input.replace(" HTTP/1.0\r\n", ""); // Theoretically by this point, the request // will have been cut down to just the // requested resource, but in my experience // this code is gonna result in like 50k errors // and I'm gonna have to rewrite it to get it // to actually work the way I want it to. if input != "/" { let mut chars = input.chars(); chars.next(); let path = chars.as_str().to_string(); let exists = check_if_path_exists(path); let path = chars.as_str().to_string(); if exists == false && path.contains(".cgi?") == false { output = ".error_server_404"; } else { let path = chars.as_str().to_string(); let dir = check_if_dir(path); if dir == true { let path = chars.as_str().to_string(); index += &path; index += "/index.html"; output = index.as_str().clone(); } else { output = chars.as_str(); } } } else { output = "index.html"; } } else { // It's get_page()'s problem now. println!("Stream sent unhandlable request."); output = ".error_server_503"; } // Did you want to see chars.as_str().to_string()? return output.to_string(); } fn serve(mut stream: TcpStream) { thread::spawn(move || { println!("Stream thread created."); let mut request = Vec::new(); let mut reader = BufReader::new(&mut stream); reader .read_until(b'\n', &mut request) .expect("Failed to read from stream!"); let resource = process_request(request); // Haha, bodged my way around Rust's ownership paradigm! let mime; if resource.to_string().contains(".error") { mime = "text/html".to_string(); } else { mime = detect_media_type(resource.to_string()); } let contents = get_page(resource); let header = "HTTP/1.1 200 OK\r\n"; let content_type = format!("Content-Type: {}\r\n", mime); let server = "Server: Herb/0.3.0\r\n"; let extra_fields; if cfg!(unix) { let mut time = format!("Date: {}\r\n", grab_time()); time = time.replace("'",""); extra_fields = format!("{}{}\r\n", server, time); } else { // I don't have a Windows or macOS box to test anything on // which means others are gonna have to deal with it :/ extra_fields = format!("{}\r\n", server); } let response = format!("{}{}{}{}", header, content_type, extra_fields, contents); stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); }); } fn main() -> std::io::Result<()> { let listen = TcpListener::bind("0.0.0.0:8080")?; for stream in listen.incoming() { println!("Serving incoming stream."); serve(stream?); } Ok(()) }