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.1\"")
.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, format!("\"./{}\"", file.as_ref().unwrap().path().display().to_string()), file.unwrap().file_name());
}
return format!("{}
Generated by herb 0.3.1", 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.1\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(())
}