dockerise

This commit is contained in:
2025-03-13 13:52:33 +01:00
parent 4e1ae2bfa1
commit 0242b29f3c
5 changed files with 129 additions and 73 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
target/
debug/
**/*.rs.bk

View File

@ -4,3 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]

30
Dockerfile Normal file
View File

@ -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"]

15
compose.yml Normal file
View File

@ -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

View File

@ -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<RequestLine> {
fn parse_start_line(input: &str) -> Result<RequestLine, Vec<u8>> {
let mut response_field_lines: HashMap<String, String> = HashMap::new();
let mut response_body: Vec<u8> = 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::<Vec<&str>>();
let vec = input.trim().split_ascii_whitespace().collect::<Vec<&str>>();
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<RequestLine> {
}
}
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<HashMap<String, String>> {
@ -198,7 +230,7 @@ fn parse_field_lines(reader: &mut BufReader<&mut TcpStream>) -> Option<HashMap<S
fn response_builder(
method: RequestMethods,
status_line: String,
status_line: &str,
field_lines: HashMap<String, String>,
body: Vec<u8>,
) -> Vec<u8> {
@ -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<u8> = 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<String, String> = HashMap::new();
let body: Vec<u8> = 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(())
}