From 199a7b891fba456bc2597d2eebdbe845bd815388 Mon Sep 17 00:00:00 2001 From: Triston Armstrong Date: Thu, 9 Mar 2023 01:01:43 +0700 Subject: [PATCH] finish majority of http server implimentation while learning Rust --- .gitignore | 1 + Cargo.lock | 7 ++ Cargo.toml | 8 +++ public/hello.html | 14 ++++ public/index.html | 15 ++++ public/style.css | 11 +++ src/http/method.rs | 37 ++++++++++ src/http/mod.rs | 11 +++ src/http/query_string.rs | 43 +++++++++++ src/http/request.rs | 151 +++++++++++++++++++++++++++++++++++++++ src/http/response.rs | 33 +++++++++ src/http/status_code.rs | 24 +++++++ src/main.rs | 25 +++++++ src/server.rs | 56 +++++++++++++++ src/website_handler.rs | 52 ++++++++++++++ 15 files changed, 488 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 public/hello.html create mode 100644 public/index.html create mode 100644 public/style.css create mode 100644 src/http/method.rs create mode 100644 src/http/mod.rs create mode 100644 src/http/query_string.rs create mode 100644 src/http/request.rs create mode 100644 src/http/response.rs create mode 100644 src/http/status_code.rs create mode 100644 src/main.rs create mode 100644 src/server.rs create mode 100644 src/website_handler.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8254b11 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "http_server" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9bccf2c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "http_server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/public/hello.html b/public/hello.html new file mode 100644 index 0000000..4606ee1 --- /dev/null +++ b/public/hello.html @@ -0,0 +1,14 @@ + + + + + + + Hello + + + +

Hello

+

This is the hello.html file that you were looking for

+ + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..3ab0e71 --- /dev/null +++ b/public/index.html @@ -0,0 +1,15 @@ + + + + + + + Main + + + +

Index

+

This is the index.html file you were looking for

+ + + \ No newline at end of file diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..df87edb --- /dev/null +++ b/public/style.css @@ -0,0 +1,11 @@ +h1 { + color: rgb(169, 61, 215); +} + +button { + background-color: rgb(169, 61, 215); + color: white; + font-weight: bold; + border: none; + border-radius: 6px; +} \ No newline at end of file diff --git a/src/http/method.rs b/src/http/method.rs new file mode 100644 index 0000000..cdb19c7 --- /dev/null +++ b/src/http/method.rs @@ -0,0 +1,37 @@ +use std::str::FromStr; + +#[derive(Debug)] +pub enum Method { + POST, + GET, + OPTIONS, + PUT, + DELETE, + HEAD, + CONNECT, + TRACE, + PATCH +} + +impl FromStr for Method { + type Err = MethodError; + + // this is a method override on the str struct. When we call parse on a str meant to become a Method type + // it will call this from_str method passing itself to the method and returning a Method type + fn from_str(s: &str) -> Result { + match s { + "POST" => Ok(Self::POST), + "GET" => Ok(Self::GET), + "OPTIONS" => Ok(Self::OPTIONS), + "PUT" => Ok(Self::PUT), + "DELETE" => Ok(Self::DELETE), + "HEAD" => Ok(Self::HEAD), + "CONNECT" => Ok(Self::CONNECT), + "TRACE" => Ok(Self::TRACE), + "PATCH" => Ok(Self::PATCH), + _ => Err(MethodError) + } + } +} + +pub struct MethodError; \ No newline at end of file diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 0000000..ec1676e --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,11 @@ +pub use request::{Request, ParserError}; +pub use method::Method; +pub use query_string::{QueryString, Value as QueryStringValue}; +pub use response::Response; +pub use status_code::StatusCode; + +pub mod request; +pub mod method; +pub mod query_string; +pub mod response; +pub mod status_code; \ No newline at end of file diff --git a/src/http/query_string.rs b/src/http/query_string.rs new file mode 100644 index 0000000..c97727b --- /dev/null +++ b/src/http/query_string.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +#[derive(Debug)] +pub struct QueryString<'buf> { + data: HashMap<&'buf str, Value<'buf>> +} + +#[derive(Debug)] +pub enum Value<'buf> { + SINGLE(&'buf str), + MULTIPLE(Vec<&'buf str>) +} + +impl<'buf> QueryString<'buf> { + pub fn get(&self, key: &str) -> Option<&Value>{ + self.data.get(key) + } +} + +// From is used when the conversion cannot fail +impl<'buf> From<&'buf str> for QueryString<'buf>{ + fn from(value: &'buf str) -> Self { + let mut data = HashMap::new(); + for sub_str in value.split('&'){ + let mut key = sub_str; + let mut val = ""; + if let Some(i) = sub_str.find('='){ + key = &sub_str[..i]; + val = &sub_str[i+1..]; + } + + data.entry(key) + .and_modify(|existing| match existing { + Value::SINGLE(prev) => { + *existing = Value::MULTIPLE(vec![prev, val]); + }, + Value::MULTIPLE(vec) => vec.push(val) + }) + .or_insert(Value::SINGLE(val)); + } + QueryString{data} + } +} \ No newline at end of file diff --git a/src/http/request.rs b/src/http/request.rs new file mode 100644 index 0000000..37c52b4 --- /dev/null +++ b/src/http/request.rs @@ -0,0 +1,151 @@ +use super::method::{Method, MethodError}; +use super::QueryString; +use std::collections::HashMap; +use std::error::Error; +use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; +use std::str; +use std::str::Utf8Error; + +#[derive(Debug)] +pub struct Request<'buf> { + path: &'buf str, // must specify lifetime for reference inside struct + query_string: Option>, + method: Method, + headers: Headers +} + +impl<'buf> Request<'buf> { + pub fn path(&self) -> &str { + &self.path + } + pub fn method(&self) -> &Method { + &self.method + } + pub fn query_string(&self) -> Option<&QueryString<'buf>> { + self.query_string.as_ref() + } +} + + +// TryFrom is used when the conversion can fail +// 👇🏻 - lifetime identifier +impl<'buf> TryFrom<&'buf [u8]> for Request<'buf> { + type Error = ParserError; + + // GET /search?name=abc&sort=1 HTTP/1.1 + fn try_from(buf: &'buf [u8]) -> Result { + // convert u8 buffer array to utf8 encoded string + let request = str::from_utf8(buf)?; + + // gets the first chunk from a request, ends up as ("GET", ".. rest") + let (method, request) = get_next_word(request).ok_or(ParserError::InvalidMethod)?; // GET + + // gets the first chunk from ".. rest", ends up as ("/search?name=abc&sort=1", ".. rest") + let (mut path, request) = get_next_word(request).ok_or(ParserError::InvalidMethod)?; // /search?name=abc&sort=1 + + // gets the first chunk from ".. rest", ends up as ("HTTP/1.1", ".. rest") + let (protocol, headers) = get_next_word(request).ok_or(ParserError::InvalidMethod)?; // HTTP/1.1 + + // we only handle this protocol + if protocol != "HTTP/1.1" { + return Err(ParserError::InvalidProtocol); + } + + // goto Method.rs - to see how this works + let method: Method = method.parse()?; + + let mut query_string = None; + // get the byte index from path of the "?" and grab all bytes from that position to end of path to get all queries + // also grab the path from pre "?" + if let Some(i) = path.find('?') { + query_string = Some(QueryString::from(&path[i + 1..])); // adds 1 byte to i - not one character + path = &path[..i]; + } + + // returns an instance of Request with all of the necessary information + Ok(Self { + path, + query_string, + method, + headers: Headers::new(headers.to_string()) + }) + } +} + +// read the request string slice up to a space or return and return a slice of the pre space and the post space +fn get_next_word(request: &str) -> Option<(&str, &str)> { + for (index, char) in request.chars().enumerate() { + if char == ' ' || char == '\r' { + return Some((&request[..index], &request[index + 1..])); + } + } + None +} + +pub enum ParserError { + InvalidRequest, + InvalidEncoding, + InvalidProtocol, + InvalidMethod, +} + +impl ParserError { + fn message(&self) -> &str { + match self { + Self::InvalidRequest => "Invalid request", + Self::InvalidEncoding => "Invalid encoding", + Self::InvalidProtocol => "Invalid Protocol", + Self::InvalidMethod => "Invalid Method", + } + } +} + +impl Error for ParserError { + // by implimenting this ourselves itll force us to meet some expectations for our error types +} + +impl From for ParserError { + fn from(_: MethodError) -> Self { + Self::InvalidMethod + } +} + +impl From for ParserError { + fn from(_: Utf8Error) -> Self { + Self::InvalidEncoding + } +} + +impl Display for ParserError { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "{}", self.message()) + } +} + +impl Debug for ParserError { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "{}", self.message()) + } +} + +#[derive(Debug)] +struct Headers { + headers_map: HashMap +} + +impl Headers{ + fn new(headers: String) -> Self{ + let mut map: HashMap = HashMap::new(); + let split_headers: Vec<&str> = headers.split("\r\n").collect(); + for header in split_headers.iter(){ + let split_header: Vec<&str> = header.split(": ").collect(); + if split_header.len() < 2 { + continue; + } + let key = split_header[0]; + let value = split_header[1]; + map.insert(key.to_owned(), value.to_owned()); + } + Self { headers_map: map } + } +} \ No newline at end of file diff --git a/src/http/response.rs b/src/http/response.rs new file mode 100644 index 0000000..54a691d --- /dev/null +++ b/src/http/response.rs @@ -0,0 +1,33 @@ +use super::StatusCode; +use std::{fmt::{Display, Formatter, Result as FmtResult}}; +use std::io::{Result as IoResult, Write}; + +#[derive(Debug)] +pub struct Response{ + status_code: StatusCode, + body: Option +} + +impl Response { + pub fn new(status_code: StatusCode, body: Option) -> Self { + Response { status_code, body } + } + + pub fn send(&self, stream: &mut impl Write) -> IoResult<()> { + let body = match &self.body{ + Some(b) => b, + None => "" + }; + write!(stream, "HTTP/1.1 {} {}\r\n\r\n{}", self.status_code, self.status_code.reason_phrase(), body) + } +} + +impl Display for Response { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + let body = match &self.body{ + Some(b) => b, + None => "" + }; + write!(f, "HTTP/1.1 {} {}\r\n\r\n{}", self.status_code, self.status_code.reason_phrase(), body) + } +} \ No newline at end of file diff --git a/src/http/status_code.rs b/src/http/status_code.rs new file mode 100644 index 0000000..3f34d87 --- /dev/null +++ b/src/http/status_code.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result as FmtResult}; + +#[derive(Clone, Copy, Debug)] +pub enum StatusCode { + OK = 200, + BadRequest = 400, + NotFound = 404, +} + +impl StatusCode { + pub fn reason_phrase(&self) -> &str { + match self { + Self::OK => "OK", + Self::BadRequest => "Bad Request", + Self::NotFound => "Not Found", + } + } +} + +impl Display for StatusCode { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "{}", *self as u16) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d48c94b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,25 @@ + +#![allow(dead_code)] +use server::Server; +use website_handler::WebsiteHandler; +use std::env; + +mod server; +mod http; +mod website_handler; + +fn main() { + // create default path incase public path isnt provided + let defualt_path = format!("{}/public", env!("CARGO_MANIFEST_DIR")); + // if public path is provided we unwrap it or default to default path + let public_path = env::var("PUBLIC").unwrap_or(defualt_path); + + let address = String::from("localhost:8080"); + let server = Server::new(address); + // instantiate new website handler instance with "public path" vairable as parameter + server.run(WebsiteHandler::new(public_path)); +} + +/* +GET /user?id=10 HTTP/1.1\r\n + */ \ No newline at end of file diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..95b880f --- /dev/null +++ b/src/server.rs @@ -0,0 +1,56 @@ +use std::net::{TcpListener, TcpStream}; +use std::convert::TryFrom; +use std::io::{Read}; +use crate::http::{Request, Response, StatusCode, ParserError}; + +pub trait Handler { + fn handle_request(&mut self, request: &Request) -> Response; + fn handle_bad_request(&mut self, e: &ParserError) -> Response { + println!("Failed to handle request: {:?}", e); + Response::new(StatusCode::BadRequest, None) + } +} + + +pub struct Server{ + addr: String +} + +impl Server { + // returns a new instance of this Server struct + pub fn new(addr: String) -> Self{ + Self { addr } + } + + // starts an infinite loop that accepts server connections + pub fn run(self, mut handler: impl Handler){ + println!("Server listening on: {}", &self.addr[10..]); + let listener = TcpListener::bind(&self.addr).unwrap(); + loop { + match listener.accept() { + Ok((mut stream, _)) => Self::handle_client(&mut stream, &mut handler), + Err(e) => println!("Failed to establish a connection: {}", e), + } + } + } + + fn handle_client(stream: &mut TcpStream, handler: &mut impl Handler){ + // Only read a buffer of up to 1024 bytes + let mut buffer = [0; 1024]; + // read from stream using match to get an oK from the result returned from read + match stream.read(&mut buffer){ + Ok(_)=> { + // using the Request try_from method to read the buffer -> goto Request.rs + let response = match Request::try_from(&buffer[..]){ + Ok(request) => handler.handle_request(&request), + Err(e) => handler.handle_bad_request(&e) + }; + + if let Err(e) = response.send(stream){ + println!("Failed to send response: {}", e); + } + }, + Err(e) => println!("Failed to read from connection: {}", e) + } + } +} \ No newline at end of file diff --git a/src/website_handler.rs b/src/website_handler.rs new file mode 100644 index 0000000..cdfd648 --- /dev/null +++ b/src/website_handler.rs @@ -0,0 +1,52 @@ +use super::http::{Method, Request, Response, StatusCode}; +use super::server::Handler; +use std::fs; +pub struct WebsiteHandler { + public_path: String, +} + +impl WebsiteHandler { + pub fn new(public_path: String) -> Self { + Self { public_path } + } + fn read_file(&self, file_path: &str) -> Option { + let path = format!("{}/{}", self.public_path, file_path); + match fs::canonicalize(path) { + Ok(path) => { + if path.starts_with(&self.public_path) { + return fs::read_to_string(path).ok(); + } + println!("Directory traversal attack attempted: {}", file_path); + None + } + Err(_) => None, + } + } +} + +// This is where all of the routing is being handled +impl Handler for WebsiteHandler { + fn handle_request(&mut self, request: &Request) -> Response { + match request.method() { + // Creating routes + Method::GET => match request.path() { + "/" => Response::new(StatusCode::OK, self.read_file("index.html")), + "/hello" => Response::new(StatusCode::OK, self.read_file("hello.html")), + path => match self.read_file(path) { + Some(file) => Response::new(StatusCode::OK, Some(file)), + None => Response::new(StatusCode::NotFound, None), + }, + }, + Method::POST => todo!(), + Method::OPTIONS => todo!(), + Method::PUT => todo!(), + Method::DELETE => todo!(), + Method::HEAD => todo!(), + Method::CONNECT => todo!(), + Method::TRACE => todo!(), + Method::PATCH => todo!(), + _ => Response::new(StatusCode::NotFound, None), + } + } +} +