Compare commits
	
		
			7 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9adeaa8ff9 | |||
| 2e22186952 | |||
| feee7b53cc | |||
| 8ba1b04f08 | |||
| 54d0b65300 | |||
| 3a6ce349d9 | |||
| 684306c25c | 
| @@ -1,3 +0,0 @@ | ||||
| target/ | ||||
| debug/ | ||||
| **/*.rs.bk | ||||
							
								
								
									
										14
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,12 +1,2 @@ | ||||
| # ---> Rust | ||||
| # Generated by Cargo | ||||
| # will have compiled files and executables | ||||
| debug/ | ||||
| target/ | ||||
|  | ||||
| # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries | ||||
| # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | ||||
| # Cargo.lock | ||||
|  | ||||
| # These are backup files generated by rustfmt | ||||
| **/*.rs.bk | ||||
| libhttp.a | ||||
| http_server | ||||
|   | ||||
							
								
								
									
										58
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										58
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -1,58 +0,0 @@ | ||||
| # This file is automatically @generated by Cargo. | ||||
| # It is not intended for manual editing. | ||||
| version = 4 | ||||
|  | ||||
| [[package]] | ||||
| name = "http_server" | ||||
| version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "mime_guess", | ||||
|  "signal-hook", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "libc" | ||||
| version = "0.2.171" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" | ||||
|  | ||||
| [[package]] | ||||
| name = "mime" | ||||
| version = "0.3.17" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | ||||
|  | ||||
| [[package]] | ||||
| name = "mime_guess" | ||||
| version = "2.0.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" | ||||
| dependencies = [ | ||||
|  "mime", | ||||
|  "unicase", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "signal-hook" | ||||
| version = "0.3.17" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
|  "signal-hook-registry", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "signal-hook-registry" | ||||
| version = "1.4.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "unicase" | ||||
| version = "2.8.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" | ||||
| @@ -1,8 +0,0 @@ | ||||
| [package] | ||||
| name = "http_server" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| [dependencies] | ||||
| signal-hook = "0.3.17" | ||||
| mime_guess = "2.0.5" | ||||
							
								
								
									
										36
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,36 +0,0 @@ | ||||
| FROM rust:1.85-slim-bookworm AS build | ||||
|  | ||||
| # get x86_64-unknown-linux-musl as a build target | ||||
| RUN rustup target add x86_64-unknown-linux-musl | ||||
|  | ||||
| # 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-musl | ||||
| RUN strip /http_server/target/x86_64-unknown-linux-musl/release/http_server | ||||
|  | ||||
| FROM scratch | ||||
|  | ||||
| # copy the build artifact from the build stage | ||||
| COPY --from=build /http_server/target/x86_64-unknown-linux-musl/release/http_server / | ||||
| COPY ./www /www | ||||
| COPY ./internal /internal | ||||
|  | ||||
| EXPOSE 8080 | ||||
|  | ||||
| CMD ["/http_server"] | ||||
							
								
								
									
										8
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| .POSIX: | ||||
| server: server.c libhttp.a | ||||
| 	gcc -I . -L. -ansi -ggdb -o http_server server.c -lstrops -Wl,-Bstatic -lhttp -Wl,-Bdynamic | ||||
|  | ||||
| libhttp.a: http.c | ||||
| 	gcc -c -ansi -ggdb -o http.o http.c -lstrops | ||||
| 	ar cr libhttp.a http.o | ||||
| 	rm http.o | ||||
							
								
								
									
										15
									
								
								compose.yml
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								compose.yml
									
									
									
									
									
								
							| @@ -1,15 +0,0 @@ | ||||
| networks: | ||||
|   http-network: | ||||
|     name: http-network | ||||
|     driver: bridge | ||||
|  | ||||
| services: | ||||
|   http-server: | ||||
|      container_name: http-server | ||||
|      build: . | ||||
|      image: http-server:latest | ||||
|      restart: always | ||||
|      ports: | ||||
|        - 80:8080 | ||||
|      networks: | ||||
|        - http-network | ||||
							
								
								
									
										104
									
								
								http.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								http.c
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| #include "http.h" | ||||
| #include <strops.h> | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <string.h> | ||||
|  | ||||
| /* TODO: https://blog.netherlabs.nl/articles/2009/01/18/the-ultimate-so_linger-page-or-why-is-my-tcp-not-reliable */ | ||||
| int http_init(int port, int connection_amount) { | ||||
|     int ret; | ||||
|     int server_sock = socket(AF_INET, SOCK_STREAM, 0); | ||||
|     if (server_sock == -1) { | ||||
|         perror("Couldn't create socket"); | ||||
|         return -1; | ||||
|     } | ||||
|  | ||||
|     struct sockaddr_in server_addr; | ||||
|     server_addr.sin_family = AF_INET; | ||||
|     server_addr.sin_addr.s_addr = INADDR_ANY; | ||||
|     server_addr.sin_port = htons(port); | ||||
|  | ||||
|     if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof server_addr) == -1) { | ||||
|         perror("Couldn't bind socket"); | ||||
|         return -1; | ||||
|     } | ||||
|  | ||||
|     if (listen(server_sock, connection_amount) == -1) { | ||||
|         perror("Cannot listen on socket"); | ||||
|         return -1; | ||||
|     } | ||||
|  | ||||
|     return server_sock; | ||||
| } | ||||
|  | ||||
| HTTP_Request* http_accept(int server) { | ||||
|     int client_sock = accept(server, NULL, NULL); | ||||
|     if (client_sock == -1) { | ||||
|         perror("Couldn't connect to client"); | ||||
|         return NULL; | ||||
|     } | ||||
|  | ||||
|     HTTP_Request *request = malloc(sizeof(HTTP_Request)); | ||||
|     request->client_sock = client_sock; | ||||
|  | ||||
|     /* TODO: Read entire message and parse into request struct */ | ||||
|     ssize_t bytes_read; | ||||
|     size_t bufsize = 4096; | ||||
|     char buf[bufsize]; | ||||
|  | ||||
|     bytes_read = read(request->client_sock, buf, bufsize - 1); | ||||
|     if (bytes_read == -1) { | ||||
|         perror("Failed to read data"); | ||||
|     } | ||||
|  | ||||
|     /* TODO: read tmp line by line */ | ||||
|     char *tmp = strops_trim_left_string(buf, "\r\n"); | ||||
|     size_t lines_count = 0; | ||||
|     size_t lines_capacity = 10; | ||||
|     char **lines = malloc(lines_capacity * sizeof (char*)); | ||||
|     size_t i; | ||||
|     while (strops_length(tmp) > 0) { | ||||
|         char *line = malloc(strops_length(tmp)); | ||||
|         memset(line, 0, strops_length(tmp)); | ||||
|         for (i = 0; i < strops_length(tmp); i++) { | ||||
|             if (tmp[i] == '\r' && tmp[i + 1] == '\n') { | ||||
|                 i += 2; | ||||
|                 break; | ||||
|             } | ||||
|             line[i] = tmp[i]; | ||||
|         } | ||||
|         for (; i > 0; i--) { | ||||
|             strops_remove_at_pos_char_inplace(tmp, 0); | ||||
|         } | ||||
|  | ||||
|         if (lines_count >= lines_capacity) { | ||||
|             lines_capacity *= 2; | ||||
|             lines = realloc(lines, lines_capacity * sizeof (char*)); | ||||
|         } | ||||
|         lines[lines_count] = line; | ||||
|         lines_count++; | ||||
|     } | ||||
|     free(tmp); | ||||
|     /* TODO: Parse lines */ | ||||
|     /* TODO: find suitable data structure for field-lines */ | ||||
|     /* | ||||
|         request-line => method target version | ||||
|         seperated by whitespace (SP) | ||||
|  | ||||
|         field-line => field-name:field-value | ||||
|         seperated by a single colon => ':' | ||||
|         field-value may have leading and trailing optionial whitespace (OWS) | ||||
|     */ | ||||
|     for (i = 0; i < lines_count; i++) { | ||||
|         printf("%s\n", lines[i]); | ||||
|     } | ||||
|      | ||||
|     /* TODO: remeber to delete this code after finishing parsing code */ | ||||
|     for (i = lines_count; i > 0; i--) { | ||||
|         free(lines[i]); | ||||
|     } | ||||
|     free(lines); | ||||
|  | ||||
|     return request; | ||||
| } | ||||
|  | ||||
							
								
								
									
										26
									
								
								http.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								http.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| #ifndef HTTP_H | ||||
| #define HTTP_H | ||||
|  | ||||
| #include <netinet/in.h> | ||||
| #include <sys/socket.h> | ||||
|  | ||||
| typedef struct { | ||||
|     int client_sock; | ||||
|     char *method; | ||||
|     char *target; | ||||
|     char *version; | ||||
|     /* Field_Lines (Header) */ | ||||
|     char *body; | ||||
| } HTTP_Request; | ||||
|  | ||||
| typedef struct { | ||||
|     char *version; | ||||
|     unsigned int status; | ||||
|     /* Field_Lines (Header) */ | ||||
|     char *body; | ||||
| } HTTP_Response; | ||||
|  | ||||
| int http_init(int port, int connection_amount); | ||||
| HTTP_Request* http_accept(int server); | ||||
|  | ||||
| #endif /* HTTP_H */ | ||||
							
								
								
									
										48
									
								
								server.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								server.c
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| #include <strops.h> | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <errno.h> | ||||
|  | ||||
| #include "http.h" | ||||
| #include <unistd.h> | ||||
|  | ||||
| #define PORT 8080 | ||||
|  | ||||
| HTTP_Response* handle_client(HTTP_Request *request) { | ||||
|     ssize_t bytes_written; | ||||
|      | ||||
|     char *response = "HTTP/1.1 200 \r\n\r\n"; | ||||
|     bytes_written = write(request->client_sock, response, strops_length(response)); | ||||
|     if (bytes_written != strops_length(response)) { | ||||
|         fprintf(stderr, "Incomplete write\n"); | ||||
|     } | ||||
|  | ||||
|     return NULL; | ||||
| } | ||||
|  | ||||
| /* | ||||
|     TODO: implement signals | ||||
|     TODO: graceful server shutdown | ||||
| */ | ||||
| int main() { | ||||
|     int ret; | ||||
|     int server; | ||||
|     server = http_init(PORT, 3); | ||||
|     if (server == -1) { | ||||
|         return 1; | ||||
|     } | ||||
|      | ||||
|     HTTP_Request *request; | ||||
|     while(1) { | ||||
|         request = http_accept(server); | ||||
|         if (request == NULL) { | ||||
|             break; | ||||
|         } | ||||
|         handle_client(request); | ||||
|         close(request->client_sock); | ||||
|         free(request); | ||||
|     } | ||||
|  | ||||
|     close(server); | ||||
|     return 0; | ||||
| } | ||||
							
								
								
									
										601
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										601
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,601 +0,0 @@ | ||||
| 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::<Vec<&str>>(); | ||||
|             if version_numbers.len() != 2 { | ||||
|                 return false; | ||||
|             } | ||||
|             let major = match version_numbers[0].parse::<u8>() { | ||||
|                 Ok(val) => val, | ||||
|                 Err(_) => { | ||||
|                     return false; | ||||
|                 } | ||||
|             }; | ||||
|             let minor = match version_numbers[1].parse::<u8>() { | ||||
|                 Ok(val) => val, | ||||
|                 Err(_) => { | ||||
|                     return false; | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             return major <= 9 && minor <= 9; | ||||
|         } | ||||
|  | ||||
|         false | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn parse_start_line(input: &str) -> Result<StartLine, Vec<u8>> { | ||||
|     let mut response_field_lines: HashMap<String, Vec<String>> = HashMap::new(); | ||||
|     let mut response_body: Vec<u8> = vec![]; | ||||
|     let mut start_line = StartLine::new(); | ||||
|     let vec = input.trim().split_ascii_whitespace().collect::<Vec<&str>>(); | ||||
|  | ||||
|     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"), | ||||
|             vec![response_body.len().to_string()], | ||||
|         ); | ||||
|         response_field_lines.insert( | ||||
|             String::from("content-type"), | ||||
|             vec![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, | ||||
|         )); | ||||
|     } | ||||
|     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"), | ||||
|             vec![response_body.len().to_string()], | ||||
|         ); | ||||
|         response_field_lines.insert( | ||||
|             String::from("content-type"), | ||||
|             vec![String::from("text/plain")], | ||||
|         ); | ||||
|  | ||||
|         return Err(response_builder( | ||||
|             RequestMethods::Head, | ||||
|             "HTTP/1.1 505 ", | ||||
|             Some(response_field_lines), | ||||
|             Some(response_body), | ||||
|         )); | ||||
|     } | ||||
|     if !StartLine::is_valid_target(target) { | ||||
|         return Err(response_builder( | ||||
|             RequestMethods::Head, | ||||
|             "HTTP/1.1 400 ", | ||||
|             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, | ||||
|     } | ||||
|     start_line.version = version.to_string(); | ||||
|     start_line.target = target.to_string(); | ||||
|  | ||||
|     Ok(start_line) | ||||
| } | ||||
|  | ||||
| // Example -> Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8 | ||||
| // Be careful of optional whitespace (OWS) and CRLF | ||||
| // q=x.yyy is basically a value which says, what the client wants most. | ||||
| // The closer to 1 the better. If no q=x.yyy was specified, then q=1 is implied. | ||||
| // This basically mean I should sort the vector by qvalue. So that the one it wants most, is at the beginning. | ||||
| fn parse_field_line(field_line: (&str, &str)) -> (String, Vec<String>) { | ||||
|     let field_name = field_line.0.to_ascii_lowercase(); | ||||
|     let mut fuck: HashMap<String, Vec<String>> = HashMap::new(); | ||||
|  | ||||
|     if !field_line.1.contains(",") { | ||||
|         return (field_name, vec![field_line.1.trim().to_owned()]); | ||||
|     } | ||||
|     let temp_field_values = field_line.1.split(",").collect::<Vec<&str>>(); | ||||
|  | ||||
|     for field_value in temp_field_values { | ||||
|         if field_value.contains(";") { | ||||
|             let temp_field_value = field_value.split(";").collect::<Vec<&str>>(); | ||||
|             if temp_field_value.len() != 2 { | ||||
|                 // TODO: return with an http_response | ||||
|                 continue; | ||||
|             } | ||||
|             if !temp_field_value[1].starts_with("q=") && !temp_field_value[1].starts_with("Q=") { | ||||
|                 // TODO: return with an http_response | ||||
|                 continue; | ||||
|             } | ||||
|             let qvalue = temp_field_value[1] | ||||
|                 .trim() | ||||
|                 .trim_start_matches("q=") | ||||
|                 .trim_start_matches("Q="); | ||||
|  | ||||
|             if qvalue == "0" { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if let Some(val) = fuck.get_mut(qvalue) { | ||||
|                 val.push(temp_field_value[0].trim().to_owned()); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             fuck.insert( | ||||
|                 qvalue.to_owned(), | ||||
|                 vec![temp_field_value[0].trim().to_owned()], | ||||
|             ); | ||||
|         } else { | ||||
|             if let Some(val) = fuck.get_mut("1") { | ||||
|                 val.push(field_value.trim().to_owned()); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             fuck.insert("1".to_owned(), vec![field_value.trim().to_owned()]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let fuck_keys = fuck.keys(); | ||||
|     let mut keys = fuck_keys.collect::<Vec<&String>>(); | ||||
|     keys.sort_by(|a, b| b.cmp(a)); | ||||
|  | ||||
|     let mut field_values: Vec<String> = vec![]; | ||||
|     for key in keys { | ||||
|         if let Some(value) = fuck.get(key) { | ||||
|             value.iter().for_each(|val| field_values.push(val.clone())); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     (field_name, field_values) | ||||
| } | ||||
|  | ||||
| fn parse_field_lines( | ||||
|     reader: &mut BufReader<&mut TcpStream>, | ||||
| ) -> Result<HashMap<String, Vec<String>>, Vec<u8>> { | ||||
|     let mut response_field_lines: HashMap<String, Vec<String>> = HashMap::new(); | ||||
|     let mut response_body: Vec<u8> = vec![]; | ||||
|     let mut field_lines: HashMap<String, Vec<String>> = 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"), | ||||
|                 vec![response_body.len().to_string()], | ||||
|             ); | ||||
|             response_field_lines.insert( | ||||
|                 String::from("content-type"), | ||||
|                 vec![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: (String, Vec<String>) = match line.split_once(":") { | ||||
|             Some(val) => parse_field_line(val), | ||||
|             None => { | ||||
|                 "Invalid field-line" | ||||
|                     .as_bytes() | ||||
|                     .iter() | ||||
|                     .for_each(|byte| response_body.push(*byte)); | ||||
|  | ||||
|                 response_field_lines.insert( | ||||
|                     String::from("content-length"), | ||||
|                     vec![response_body.len().to_string()], | ||||
|                 ); | ||||
|                 response_field_lines.insert( | ||||
|                     String::from("content-type"), | ||||
|                     vec![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, field_line.1); | ||||
|     } | ||||
|  | ||||
|     if !field_lines.contains_key(&String::from("host")) { | ||||
|         "field-line with field-name -> host is missing" | ||||
|             .as_bytes() | ||||
|             .iter() | ||||
|             .for_each(|byte| response_body.push(*byte)); | ||||
|  | ||||
|         response_field_lines.insert( | ||||
|             String::from("content-length"), | ||||
|             vec![response_body.len().to_string()], | ||||
|         ); | ||||
|         response_field_lines.insert( | ||||
|             String::from("content-type"), | ||||
|             vec![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<HashMap<String, Vec<String>>>, | ||||
|     body: Option<Vec<u8>>, | ||||
| ) -> Vec<u8> { | ||||
|     let mut response: Vec<u8> = 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' '); | ||||
|  | ||||
|             for val in field_line.1 { | ||||
|                 val.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<String, Vec<String>>) -> Vec<u8> { | ||||
|     let mut response_field_lines: HashMap<String, Vec<String>> = HashMap::new(); | ||||
|     let mut response_body: Vec<u8> = vec![]; | ||||
|     let path: PathBuf = match start_line.target.as_str() { | ||||
|         "/" => PathBuf::from("/www/index.html"), | ||||
|         _ => 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"), | ||||
|                 vec![response_body.len().to_string()], | ||||
|             ); | ||||
|  | ||||
|             let mime_type = mime_guess::from_path(&path) | ||||
|                 .first() | ||||
|                 .expect("Could not guess mime-type from path"); | ||||
|  | ||||
|             if let Some(vector) = field_lines.get("accept") { | ||||
|                 for value in vector { | ||||
|                     if mime_type.to_string() == *value { | ||||
|                         response_field_lines | ||||
|                             .insert(String::from("content-type"), vec![mime_type.to_string()]); | ||||
|                     } | ||||
|  | ||||
|                     if format!("{}/*", mime_type.type_()) == *value { | ||||
|                         response_field_lines | ||||
|                             .insert(String::from("content-type"), vec![mime_type.to_string()]); | ||||
|                     } | ||||
|  | ||||
|                     if "*/*" == *value { | ||||
|                         response_field_lines | ||||
|                             .insert(String::from("content-type"), vec![mime_type.to_string()]); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 response_field_lines | ||||
|                     .insert(String::from("content-type"), vec![mime_type.to_string()]); | ||||
|             } | ||||
|  | ||||
|             if !response_field_lines.contains_key("content-type") { | ||||
|                 // TODO: make better according to https://datatracker.ietf.org/doc/html/rfc9110#status.406 | ||||
|                 return response_builder(start_line.method, "HTTP/1.1 406 ", None, None); | ||||
|             } | ||||
|  | ||||
|             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 try_get_file_internal(start_line: &StartLine) -> Vec<u8> { | ||||
|     let mut response_field_lines: HashMap<String, Vec<String>> = HashMap::new(); | ||||
|     let mut response_body: Vec<u8> = vec![]; | ||||
|     let path: PathBuf = PathBuf::from(format!("/internal{}.html", 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"), | ||||
|                 vec![response_body.len().to_string()], | ||||
|             ); | ||||
|  | ||||
|             let mime_type = mime_guess::from_path(&path) | ||||
|                 .first() | ||||
|                 .expect("Could not guess mime-type from path"); | ||||
|  | ||||
|             response_field_lines.insert(String::from("content-type"), vec![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<dyn Error>> { | ||||
|     let mut line = String::new(); | ||||
|     let mut reader = BufReader::new(&mut stream); | ||||
|     let mut response_field_lines: HashMap<String, Vec<String>> = HashMap::new(); | ||||
|     let mut response_body: Vec<u8> = 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"), | ||||
|             vec![response_body.len().to_string()], | ||||
|         ); | ||||
|         response_field_lines.insert( | ||||
|             String::from("content-type"), | ||||
|             vec![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<u8> = 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" => try_get_file_internal(&start_line), | ||||
|         "/server-info" => try_get_file_internal(&start_line), | ||||
|         _ => try_get_file(&start_line, &field_lines), | ||||
|     }; | ||||
|     stream.write_all(&response)?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn Error>> { | ||||
|     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(()) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user