diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ef01592 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +target/ +debug/ +**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml index a8f58cf..48b8d74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e2c892f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM rust:1.85-slim-bookworm AS build + +# create a new empty shell project +RUN USER=root cargo new --bin http_server +WORKDIR /http_server + +# copy over your manifests +COPY ./Cargo.lock ./Cargo.lock +COPY ./Cargo.toml ./Cargo.toml + +# this build step will cache your dependencies +RUN cargo build --release +RUN rm src/*.rs + +# copy your source tree +COPY ./src ./src + +# build for release as a static-pie linked binary +RUN rm ./target/release/deps/http_server* +ENV RUSTFLAGS='-C target-feature=+crt-static' +RUN cargo build --release --target x86_64-unknown-linux-gnu + +FROM scratch + +# copy the build artifact from the build stage +COPY --from=build /http_server/target/x86_64-unknown-linux-gnu/release/http_server / + +EXPOSE 8080 + +CMD ["/http_server"] diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..dac61f9 --- /dev/null +++ b/compose.yml @@ -0,0 +1,15 @@ +networks: + http-network: + name: http-network + driver: bridge + +services: + http-server: + container_name: http-server + build: . + image: http-server:latest + restart: no + ports: + - 80:8080 + networks: + - http-network diff --git a/src/main.rs b/src/main.rs index 02decb8..f4924aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,17 +28,13 @@ impl RequestLine { } // https://datatracker.ietf.org/doc/html/rfc9110#name-methods - pub fn is_valid_method(method: &String) -> bool { + 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 - if [ - "GET", "HEAD", /*, "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE",*/ - ] - .contains(&method.trim()) - { + if ["GET", "HEAD"].contains(&method.trim()) { return true; } else { return false; @@ -47,7 +43,7 @@ impl RequestLine { // 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: &String) -> bool { + pub fn is_valid_target(target: &str) -> bool { if target.trim().is_empty() { return false; } @@ -78,7 +74,7 @@ impl RequestLine { return false; } - pub fn is_valid_version(version: &String) -> bool { + pub fn is_valid_version(version: &str) -> bool { if version.trim().is_empty() { return false; } @@ -115,20 +111,56 @@ impl RequestLine { } } -fn parse_start_line(input: String) -> Option { +fn parse_start_line(input: &str) -> Result> { + let mut response_field_lines: HashMap = HashMap::new(); + let mut response_body: Vec = vec![]; + if input.ends_with(" ") { - return None; + for byte in b"There is whitespace between the start-line and the first field-line\r\n" { + 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, + response_body, + )); } let mut start_line = RequestLine::new(); - let vec = input.trim().split(" ").collect::>(); + 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", + vec.len() + ); if vec.len() != 3 { - return None; + for byte in body.as_bytes() { + 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, + response_body, + )); } // start_line.method will remain RequestMethods::NULL if it is not supported. - let method = String::from(vec[0]); + let method = vec[0]; if RequestLine::is_valid_method(&method) { // TODO: Change to a switch-case if I ever support more methods @@ -140,19 +172,19 @@ fn parse_start_line(input: String) -> Option { } } - let target = String::from(vec[1]); + let target = vec[1]; if RequestLine::is_valid_target(&target) { - start_line.target = target; + start_line.target = target.to_string(); } - let version = String::from(vec[2]); + let version = vec[2]; if RequestLine::is_valid_version(&version) { - if version.trim() == "HTTP/1.1" || version.trim() == "HTTP/1.0" { - start_line.version = version; + if version == "HTTP/1.1" || version == "HTTP/1.0" { + start_line.version = version.to_string(); } } - return Some(start_line); + return Ok(start_line); } fn parse_field_lines(reader: &mut BufReader<&mut TcpStream>) -> Option> { @@ -198,7 +230,7 @@ fn parse_field_lines(reader: &mut BufReader<&mut TcpStream>) -> Option, body: Vec, ) -> Vec { @@ -255,45 +287,31 @@ fn response_builder( fn handle_request(mut stream: TcpStream) { let mut line = String::new(); let mut reader = BufReader::new(&mut stream); - reader.read_line(&mut line).unwrap(); - - let mut counter = 0; - let max_preceding_empty_lines = 1000; // Request can have one or many empty lines preceding the start-line and I will ignore these - // I will also for now only allow up to 1000 of these empty lines before I abort loop { - if counter > max_preceding_empty_lines { - stream - .write_all( - b"HTTP/1.1 400 Bad Request\r\n\r\nReceived too many preceding empty lines", - ) - .unwrap(); - return; - } + match reader.read_line(&mut line) { + Ok(val) => { + if val > 2 { + break; + } + } + // TODO: Replace with stream.write_all + Err(err) => { + eprintln!("{err}"); + return; + } + }; - // All lines MUST end with a CRLF - if !line.ends_with("\r\n") { - stream - .write_all(b"HTTP/1.1 400 Bad Request\r\n\r\nLines must end with CRLF") - .unwrap(); - return; + if line != "\r\n" { + continue; } - line = line.trim().into(); - - if !line.is_empty() { - break; - } - reader.read_line(&mut line).unwrap(); - counter += 1; } - let start_line = match parse_start_line(line) { - Some(val) => val, - None => { - stream - .write_all(b"HTTP/1.1 400 Bad Request\r\n\r\nInvalid Header") - .unwrap(); + let start_line = match parse_start_line(&line) { + Ok(val) => val, + Err(response) => { + stream.write_all(&response).unwrap(); return; } }; @@ -301,7 +319,7 @@ fn handle_request(mut stream: TcpStream) { if start_line.method == RequestMethods::NULL { stream - .write_all(b"HTTP/1.1 501 Not Implemented\r\n\r\nServer currently only supports GET") + .write_all(b"HTTP/1.1 501 Not Implemented\r\n\r\n") .unwrap(); return; } @@ -321,32 +339,21 @@ fn handle_request(mut stream: TcpStream) { // let mut body: Vec = vec![]; // reader.read_to_end(&mut body).unwrap(); // dbg!(&body); - // TODO: Act upon the request - // TODO: Figure out why this doesn't work' - let header: HashMap = HashMap::new(); - let body: Vec = vec![]; - - let response = response_builder( - start_line.method, - String::from("HTTP/1.1 200 OK"), - header, - body, - ); - - stream.write_all(&response).unwrap(); - - // stream - // .write_all(b"HTTP/1.1 200 OK\r\n\r\nHello, World!\r\n") - // .unwrap(); + stream + .write_all(b"HTTP/1.1 200 OK\r\n\r\nThis is the server speaking\r\n") + .unwrap(); } -fn main() { - let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); +fn main() -> Result<(), std::io::Error> { + let listener = TcpListener::bind("0.0.0.0:8080")?; + + println!("Server started"); for stream in listener.incoming() { - let stream = stream.unwrap(); + let stream = stream?; handle_request(stream) } + Ok(()) }