From 704a03be3de29001dd41e0eda19655dd34a268fa Mon Sep 17 00:00:00 2001 From: AustrianToast Date: Tue, 18 Mar 2025 13:13:00 +0100 Subject: [PATCH] Got it working with docker --- Dockerfile | 1 + src/main.rs | 294 ++++++++++++++++++++++++++++++++++--------------- www/index.html | 10 ++ 3 files changed, 217 insertions(+), 88 deletions(-) create mode 100644 www/index.html diff --git a/Dockerfile b/Dockerfile index e2c892f..9c78d16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ FROM scratch # copy the build artifact from the build stage COPY --from=build /http_server/target/x86_64-unknown-linux-gnu/release/http_server / +COPY ./www /www EXPOSE 8080 diff --git a/src/main.rs b/src/main.rs index cf938d6..cc52dad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,10 @@ 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, }; @@ -120,9 +122,10 @@ fn parse_start_line(input: &str) -> Result> { let mut response_body: Vec = vec![]; if input.ends_with(" ") { - for byte in b"There is whitespace between the start-line and the first field-line\r\n" { - response_body.push(*byte); - } + b"There is whitespace between the start-line and the first field-line" + .iter() + .for_each(|byte| response_body.push(*byte)); + response_field_lines.insert( String::from("Content-Length"), response_body.len().to_string(), @@ -133,7 +136,7 @@ fn parse_start_line(input: &str) -> Result> { RequestMethods::GET, "HTTP/1.1 400 Bad Request", response_field_lines, - response_body, + Some(response_body), )); } @@ -141,14 +144,15 @@ fn parse_start_line(input: &str) -> Result> { let vec = input.trim().split_ascii_whitespace().collect::>(); let body = format!( - "The start-line has an incorrect amount of items. Got the value: {}\r\n", + "The start-line has an incorrect amount of items. Got the value: {}", vec.len() ); if vec.len() != 3 { - for byte in body.as_bytes() { - response_body.push(*byte); - } + body.as_bytes() + .iter() + .for_each(|byte| response_body.push(*byte)); + response_field_lines.insert( String::from("Content-Length"), response_body.len().to_string(), @@ -159,7 +163,7 @@ fn parse_start_line(input: &str) -> Result> { RequestMethods::GET, "HTTP/1.1 400 Bad Request", response_field_lines, - response_body, + Some(response_body), )); } @@ -174,6 +178,23 @@ fn parse_start_line(input: &str) -> Result> { if method == String::from("HEAD") { start_line.method = RequestMethods::HEAD; } + } else { + b"Server only supports GET and HEAD" + .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 501 Not Implemented", + response_field_lines, + Some(response_body), + )); } let target = vec[1]; @@ -191,78 +212,120 @@ fn parse_start_line(input: &str) -> Result> { return Ok(start_line); } -fn parse_field_lines(reader: &mut BufReader<&mut TcpStream>) -> Option> { - let mut line: String; +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(); - let mut is_first_line = true; // Read field-lines till I hit an empty line loop { - line = String::new(); + let mut line = String::new(); reader.read_line(&mut line).unwrap(); - if line.starts_with(" ") && is_first_line { - return None; - } - if !line.ends_with("\r\n") { - return None; + b"Lines need to end with a CRLF" + .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 Bad Request", + response_field_lines, + Some(response_body), + )); } if line.trim().is_empty() { break; } - let field_line = line.split_once(":").expect("Shits fucked: {}"); + let field_line = match line.split_once(":") { + Some(val) => val, + None => { + b"Invalid field-line" + .iter() + .for_each(|byte| response_body.push(*byte)); - // Check if client has send more than one Host line - if field_lines.contains_key(&String::from("Host")) && field_line.0 == "Host" { - return None; - } + 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 Bad Request", + response_field_lines, + Some(response_body), + )); + } + }; field_lines.insert(field_line.0.to_owned(), field_line.1.trim().to_owned()); - - is_first_line = false; } if !field_lines.contains_key(&String::from("Host")) { - return None; + b"field-line with key HOST is missing" + .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 Bad Request", + response_field_lines, + Some(response_body), + )); } - return Some(field_lines); + return Ok(field_lines); } fn response_builder( method: RequestMethods, status_line: &str, field_lines: HashMap, - body: Vec, + body: Option>, ) -> Vec { let mut response: Vec = vec![]; - // TODO: replaces with eiter Option or Result - if status_line.is_empty() { - return response; - } - - for byte in status_line.as_bytes().iter() { - response.push(*byte); - } + status_line + .as_bytes() + .iter() + .for_each(|byte| response.push(*byte)); response.push(b'\r'); response.push(b'\n'); if !field_lines.is_empty() { for field_line in field_lines.iter() { - for byte in field_line.0.as_bytes().iter() { - response.push(*byte); - } + field_line + .0 + .as_bytes() + .iter() + .for_each(|byte| response.push(*byte)); response.push(b':'); response.push(b' '); - for byte in field_line.1.as_bytes().iter() { - response.push(*byte); - } + field_line + .1 + .as_bytes() + .iter() + .for_each(|byte| response.push(*byte)); response.push(b'\r'); response.push(b'\n'); @@ -273,81 +336,135 @@ fn response_builder( response.push(b'\r'); response.push(b'\n'); - if body.is_empty() { - return response; - } - - if method != RequestMethods::HEAD && method != RequestMethods::NULL { - for byte in body.iter() { - response.push(*byte); + if method != RequestMethods::HEAD { + match body { + Some(val) => { + val.iter().for_each(|byte| response.push(*byte)); + response.push(b'\r'); + response.push(b'\n'); + } + None => (), } - response.push(b'\r'); - response.push(b'\n'); } return response; } -fn handle_request(mut stream: TcpStream) { +fn handle_request(mut stream: TcpStream) -> Result<(), Box> { let mut line = String::new(); let mut reader = BufReader::new(&mut stream); // Request can have one or many empty lines preceding the start-line and I will ignore these loop { - match reader.read_line(&mut line) { - Ok(val) => { - if val > 2 { - break; - } - } - // TODO: Replace with stream.write_all - Err(err) => { - eprintln!("{err}"); - return; - } - }; - - if line != "\r\n" { - continue; + if reader.read_line(&mut line)? > 2 && line != "\r\n" { + break; } } let start_line = match parse_start_line(&line) { Ok(val) => val, Err(response) => { - stream.write_all(&response).unwrap(); - return; + stream.write_all(&response)?; + return Ok(()); } }; dbg!(&start_line); - if start_line.method == RequestMethods::NULL { - stream - .write_all(b"HTTP/1.1 501 Not Implemented\r\n\r\n") - .unwrap(); - return; - } - let field_lines = match parse_field_lines(&mut reader) { - Some(val) => val, - None => { - stream - .write_all(b"HTTP/1.1 400 Bad Request\r\n\r\nInvalid Header") - .unwrap(); - return; + Ok(val) => val, + Err(response) => { + stream.write_all(&response)?; + return Ok(()); } }; dbg!(&field_lines); // TODO: Read the body // let mut body: Vec = vec![]; - // reader.read_to_end(&mut body).unwrap(); + // reader.read_to_end(&mut body)?; // dbg!(&body); - // TODO: Act upon the request - stream - .write_all(b"HTTP/1.1 200 OK\r\n\r\nHello, World!\r\n") - .unwrap(); + let mut response_field_lines: HashMap = HashMap::new(); + let mut response_body: Vec = vec![]; + + // TODO: Act upon the request + if start_line.target == "/" { + let file = match fs::read("./www/index.html") { + Ok(val) => val, + Err(_) => { + b"The is no index.html, only you and me." + .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( + start_line.method, + "HTTP/1.1 200 OK", + response_field_lines, + Some(response_body), + ); + + stream.write_all(&response)?; + return Ok(()); + } + }; + + file.iter().for_each(|byte| response_body.push(*byte)); + + response_field_lines.insert(String::from("Content-Length"), file.len().to_string()); + response_field_lines.insert(String::from("Content-Type"), String::from("text/html")); + + let response = response_builder( + start_line.method, + "HTTP/1.1 200 OK", + response_field_lines, + Some(response_body), + ); + + stream.write_all(&response)?; + } else { + let path: PathBuf = PathBuf::from(format!("./www{}", start_line.target)); + + 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(), + ); + // TODO: get mime-type of file and use that here + response_field_lines.insert(String::from("Content-Type"), String::from("*/*")); + + let response = response_builder( + start_line.method, + "HTTP/1.1 200 OK", + response_field_lines, + Some(response_body), + ); + + stream.write_all(&response)?; + } + Err(_) => { + let response = response_builder( + start_line.method, + "HTTP/1.1 404 Not Found", + response_field_lines, + None, + ); + + stream.write_all(&response)?; + } + }; + } + + Ok(()) } fn main() -> Result<(), Box> { @@ -368,7 +485,8 @@ fn main() -> Result<(), Box> { for stream in listener.incoming() { let stream = stream?; - handle_request(stream); + handle_request(stream)?; } + Ok(()) } diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..afc9179 --- /dev/null +++ b/www/index.html @@ -0,0 +1,10 @@ + + + + + http-server + + +

Hello, World!

+ +