use signal_hook::{consts::*, iterator::Signals}; use std::{ collections::HashMap, error::Error, fs::{self}, io::{BufRead, BufReader, Write}, net::{TcpListener, TcpStream}, path::PathBuf, process::exit, thread, }; #[derive(PartialEq, Eq, Debug, Copy, Clone)] enum RequestMethods { Null = -1, // This is only to initialise the struct Get, Head, } #[derive(Debug)] struct StartLine { method: RequestMethods, target: String, version: String, } impl StartLine { pub fn new() -> Self { Self { method: RequestMethods::Null, target: String::new(), version: String::new(), } } // https://datatracker.ietf.org/doc/html/rfc9110#name-methods pub fn is_valid_method(method: &str) -> bool { if method.trim().is_empty() { return false; } // Only GET and HEAD are required, the rest is optional ["GET", "HEAD"].contains(&method.trim()) } // TODO: make the checks less shit and actually correct // https://datatracker.ietf.org/doc/html/rfc9112#name-request-target pub fn is_valid_target(target: &str) -> bool { if target.trim().is_empty() { return false; } // origin-form if target.starts_with("/") { if target.contains("?") && target.split("?").count() != 2 { return false; } return true; } // absolute-form if target.starts_with("http://") { return true; } // authority-form if target.contains(":") && target.split(":").count() == 2 { return true; } // asterisk-form if target.trim() == "*" { return true; } false } pub fn is_valid_version(version: &str) -> bool { if version.trim().is_empty() { return false; } let http_name = version.trim(); if http_name.starts_with("HTTP/") { let version_numbers = http_name.trim_start_matches("HTTP/"); let version_numbers = version_numbers.split(".").collect::>(); if version_numbers.len() != 2 { return false; } let major = match version_numbers[0].parse::() { Ok(val) => val, Err(_) => { return false; } }; let minor = match version_numbers[1].parse::() { Ok(val) => val, Err(_) => { return false; } }; return major <= 9 && minor <= 9; } false } } fn parse_start_line(input: &str) -> Result> { let mut response_field_lines: HashMap = HashMap::new(); let mut response_body: Vec = vec![]; let mut start_line = StartLine::new(); let vec = input.trim().split_ascii_whitespace().collect::>(); if vec.len() != 3 { format!( "The start-line has an incorrect amount of items. Got the value: {}", vec.len() ) .as_bytes() .iter() .for_each(|byte| response_body.push(*byte)); response_field_lines.insert( String::from("Content-Length"), response_body.len().to_string(), ); response_field_lines.insert(String::from("Content-Type"), String::from("text/plain")); return Err(response_builder( RequestMethods::Get, "HTTP/1.1 400 ", Some(response_field_lines), Some(response_body), )); } let method = vec[0]; let target = vec[1]; let version = vec[2]; if !StartLine::is_valid_method(method) { return Err(response_builder( RequestMethods::Head, "HTTP/1.1 501 ", None, None, )); } // start_line.method will remain RequestMethods::NULL if it is not supported. match method { "GET" => start_line.method = RequestMethods::Get, "HEAD" => start_line.method = RequestMethods::Head, _ => start_line.method = RequestMethods::Null, } if !StartLine::is_valid_version(version) { return Err(response_builder( RequestMethods::Head, "HTTP/1.1 400 ", None, None, )); } if version != "HTTP/1.1" && version != "HTTP/1.0" { "Server only supports major version 1 of HTTP" .as_bytes() .iter() .for_each(|byte| response_body.push(*byte)); response_field_lines.insert( String::from("Content-Length"), response_body.len().to_string(), ); response_field_lines.insert(String::from("Content-Type"), String::from("text/plain")); return Err(response_builder( RequestMethods::Head, "HTTP/1.1 505 ", Some(response_field_lines), Some(response_body), )); } start_line.version = version.to_string(); if !StartLine::is_valid_target(target) { return Err(response_builder( RequestMethods::Head, "HTTP/1.1 400 ", None, None, )); } start_line.target = target.to_string(); Ok(start_line) } fn parse_field_lines( reader: &mut BufReader<&mut TcpStream>, ) -> Result, Vec> { let mut response_field_lines: HashMap = HashMap::new(); let mut response_body: Vec = vec![]; let mut field_lines: HashMap = HashMap::new(); // Read field-lines till I hit an empty line loop { let mut line = String::new(); reader.read_line(&mut line).unwrap(); if !line.ends_with("\r\n") { "Lines need to end with a CRLF" .as_bytes() .iter() .for_each(|byte| response_body.push(*byte)); response_field_lines.insert( String::from("Content-Length"), response_body.len().to_string(), ); response_field_lines.insert(String::from("Content-Type"), String::from("text/plain")); return Err(response_builder( RequestMethods::Get, "HTTP/1.1 400 ", Some(response_field_lines), Some(response_body), )); } if line.trim().is_empty() { break; } let field_line = match line.split_once(":") { Some(val) => val, None => { "Invalid field-line" .as_bytes() .iter() .for_each(|byte| response_body.push(*byte)); response_field_lines.insert( String::from("Content-Length"), response_body.len().to_string(), ); response_field_lines .insert(String::from("Content-Type"), String::from("text/plain")); return Err(response_builder( RequestMethods::Get, "HTTP/1.1 400 ", Some(response_field_lines), Some(response_body), )); } }; field_lines.insert(field_line.0.to_owned(), field_line.1.trim().to_owned()); } if !field_lines.contains_key(&String::from("Host")) { "field-line with key HOST is missing" .as_bytes() .iter() .for_each(|byte| response_body.push(*byte)); response_field_lines.insert( String::from("Content-Length"), response_body.len().to_string(), ); response_field_lines.insert(String::from("Content-Type"), String::from("text/plain")); return Err(response_builder( RequestMethods::Get, "HTTP/1.1 400 ", Some(response_field_lines), Some(response_body), )); } Ok(field_lines) } fn response_builder( method: RequestMethods, status_line: &str, field_lines: Option>, body: Option>, ) -> Vec { let mut response: Vec = vec![]; status_line .as_bytes() .iter() .for_each(|byte| response.push(*byte)); response.push(b'\r'); response.push(b'\n'); if let Some(val) = field_lines { for field_line in val.iter() { field_line .0 .as_bytes() .iter() .for_each(|byte| response.push(*byte)); response.push(b':'); response.push(b' '); field_line .1 .as_bytes() .iter() .for_each(|byte| response.push(*byte)); response.push(b'\r'); response.push(b'\n'); } } // Mandatory empty line between header and body response.push(b'\r'); response.push(b'\n'); if method != RequestMethods::Head { if let Some(val) = body { val.iter().for_each(|byte| response.push(*byte)); response.push(b'\r'); response.push(b'\n'); } } response } fn try_get_file(start_line: &StartLine, _field_lines: &HashMap) -> Vec { let mut response_field_lines: HashMap = HashMap::new(); let mut response_body: Vec = vec![]; let path: PathBuf = match start_line.target.as_str() { "/" => PathBuf::from("/www/index.html"), _ => PathBuf::from(format!("/www{}", start_line.target)), }; // TODO: Check if wanted file is contained in the optional Accept field-line // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept for reference // let mime_type = match MimeGuess::from_path(&path).first_raw() { // Some(val) => val, // _ => { // return response_builder(RequestMethods::HEAD, "HTTP/1.1 500 ", None, None); // } // } match fs::read(&path) { Ok(val) => { val.iter().for_each(|byte| response_body.push(*byte)); response_field_lines.insert( String::from("Content-Length"), response_body.len().to_string(), ); let mime_type = mime_guess::from_path(&path) .first_raw() .expect("Could not guess mime-type from path"); response_field_lines.insert(String::from("Content-Type"), mime_type.to_string()); response_builder( start_line.method, "HTTP/1.1 200 ", Some(response_field_lines), Some(response_body), ) } Err(_) => response_builder(start_line.method, "HTTP/1.1 404 ", None, None), } } fn handle_request(mut stream: TcpStream) -> Result<(), Box> { let mut line = String::new(); let mut reader = BufReader::new(&mut stream); let mut response_field_lines: HashMap = HashMap::new(); let mut response_body: Vec = vec![]; // Request can have one or many empty lines preceding the start-line and I will ignore these loop { if reader.read_line(&mut line)? > 2 && line != "\r\n" { break; } } if line.ends_with(" ") { "There is whitespace between the start-line and the first field-line" .as_bytes() .iter() .for_each(|byte| response_body.push(*byte)); response_field_lines.insert( String::from("Content-Length"), response_body.len().to_string(), ); response_field_lines.insert(String::from("Content-Type"), String::from("text/plain")); let response = response_builder( RequestMethods::Get, "HTTP/1.1 400 ", Some(response_field_lines), Some(response_body), ); stream.write_all(&response)?; return Ok(()); } let start_line = match parse_start_line(&line) { Ok(val) => val, Err(response) => { stream.write_all(&response)?; return Ok(()); } }; dbg!(&start_line); let field_lines = match parse_field_lines(&mut reader) { Ok(val) => val, Err(response) => { stream.write_all(&response)?; return Ok(()); } }; dbg!(&field_lines); // let mut request_body: Vec = vec![]; // reader.read_to_end(&mut request_body)?; let response = match start_line.target.as_str() { // For docker healtcheck. If the server can properly respond, then it must be healthy. "/server-health" => response_builder(RequestMethods::Head, "HTTP/1.1 200 ", None, None), "/server-stats" => response_builder(start_line.method, "HTTP/1.1 404 ", None, None), "/server-info" => response_builder(start_line.method, "HTTP/1.1 404 ", None, None), _ => try_get_file(&start_line, &field_lines), }; stream.write_all(&response)?; Ok(()) } fn main() -> Result<(), Box> { let mut signals = Signals::new([SIGINT, SIGTERM])?; // TODO: Gracefully shutdown server thread::spawn(move || { if let Some(sig) = signals.forever().next() { println!("Received signal {:?}", sig); println!("Shutting down"); exit(1); } }); let listener = TcpListener::bind("0.0.0.0:8080")?; println!("Server started"); for stream in listener.incoming() { let stream = stream?; handle_request(stream)?; } Ok(()) }