finish majority of http server implimentation while learning Rust
This commit is contained in:
commit
199a7b891f
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal file
@ -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"
|
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@ -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]
|
14
public/hello.html
Normal file
14
public/hello.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hello</title>
|
||||||
|
<link rel="stylesheet" href="style.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello</h1>
|
||||||
|
<p>This is the hello.html file that you were looking for</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
public/index.html
Normal file
15
public/index.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Main</title>
|
||||||
|
<link rel="stylesheet" href="style.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Index</h1>
|
||||||
|
<p>This is the index.html file you were looking for</p>
|
||||||
|
<button>This button does nothing</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
11
public/style.css
Normal file
11
public/style.css
Normal file
@ -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;
|
||||||
|
}
|
37
src/http/method.rs
Normal file
37
src/http/method.rs
Normal file
@ -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<Self, Self::Err> {
|
||||||
|
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;
|
11
src/http/mod.rs
Normal file
11
src/http/mod.rs
Normal file
@ -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;
|
43
src/http/query_string.rs
Normal file
43
src/http/query_string.rs
Normal file
@ -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}
|
||||||
|
}
|
||||||
|
}
|
151
src/http/request.rs
Normal file
151
src/http/request.rs
Normal file
@ -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<QueryString<'buf>>,
|
||||||
|
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<Self, Self::Error> {
|
||||||
|
// 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<MethodError> for ParserError {
|
||||||
|
fn from(_: MethodError) -> Self {
|
||||||
|
Self::InvalidMethod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Utf8Error> 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<String, String>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Headers{
|
||||||
|
fn new(headers: String) -> Self{
|
||||||
|
let mut map: HashMap<String, String> = 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 }
|
||||||
|
}
|
||||||
|
}
|
33
src/http/response.rs
Normal file
33
src/http/response.rs
Normal file
@ -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<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
pub fn new(status_code: StatusCode, body: Option<String>) -> 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)
|
||||||
|
}
|
||||||
|
}
|
24
src/http/status_code.rs
Normal file
24
src/http/status_code.rs
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
25
src/main.rs
Normal file
25
src/main.rs
Normal file
@ -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
|
||||||
|
*/
|
56
src/server.rs
Normal file
56
src/server.rs
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
src/website_handler.rs
Normal file
52
src/website_handler.rs
Normal file
@ -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<String> {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user