├── templates ├── markdown_sample.md ├── index.html └── 404.html ├── src ├── server │ ├── mod.rs │ ├── message.rs │ ├── response.rs │ ├── worker.rs │ └── threadpool.rs ├── main.rs └── lib.rs ├── metadata.json ├── README.md ├── Cargo.toml └── .gitignore /templates/markdown_sample.md: -------------------------------------------------------------------------------- 1 | Hello world, this is a ~~complicated~~ *very simple* example. -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod message; 2 | pub mod response; 3 | pub mod threadpool; 4 | pub mod worker; 5 | -------------------------------------------------------------------------------- /src/server/message.rs: -------------------------------------------------------------------------------- 1 | use super::worker::Job; 2 | pub enum Message { 3 | NewJob(Job), 4 | Terminate, 5 | } 6 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello 5 | 6 | 7 |

Hello world

8 | 9 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 404 5 | 6 | 7 |

The requested page not found

8 | 9 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use webserver::start_server; 3 | 4 | #[derive(Parser, Debug)] 5 | struct Cli { 6 | host: String, 7 | port: String, 8 | num_of_threads: usize, 9 | } 10 | 11 | fn main() { 12 | let args = Cli::parse(); 13 | start_server(args.host, args.port, args.num_of_threads); 14 | } 15 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoints":[ 3 | { 4 | "method": "GET", 5 | "endpoint": "/", 6 | "template": "index.html" 7 | }, 8 | { 9 | "method": "GET", 10 | "endpoint": "/markdown", 11 | "template": "markdown_sample.md" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rs-webserver 2 | Lightweight multi-threading web-server written in Rust 3 | 4 | As of now, supports only static files (html and markdown) and GET method. Templates are stored in the templates folder. Endpoint config is in metadata.json. 5 | 6 | To run it 7 | `cargo run 127.0.0.1 8081 3` where the last para is num of threads for the execution. 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webserver" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = { version = "3.0", features = ["derive"] } 10 | serde = { version = "1.0.104", features = ["derive"] } 11 | serde_json = "1.0.48" 12 | pulldown-cmark = "0.9.1" 13 | -------------------------------------------------------------------------------- /src/server/response.rs: -------------------------------------------------------------------------------- 1 | pub struct Response { 2 | pub body: String, 3 | } 4 | 5 | impl Response { 6 | pub fn new(status: &str, contents: String) -> Response { 7 | let response = format!( 8 | "{}\r\nContent-Length: {}\r\n\r\n{}", 9 | status, 10 | contents.len(), 11 | contents 12 | ); 13 | 14 | Response { body: response } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb -------------------------------------------------------------------------------- /src/server/worker.rs: -------------------------------------------------------------------------------- 1 | use super::message::Message; 2 | use std::sync::mpsc; 3 | use std::sync::Arc; 4 | use std::sync::Mutex; 5 | use std::thread; 6 | 7 | pub struct Worker { 8 | pub id: usize, 9 | pub thread: Option>, 10 | } 11 | 12 | impl Worker { 13 | pub fn new(id: usize, receiver: Arc>>) -> Worker { 14 | Worker { 15 | id: id, 16 | thread: Some(thread::spawn(move || loop { 17 | let message = receiver.lock().unwrap().recv().unwrap(); 18 | 19 | match message { 20 | Message::NewJob(job) => { 21 | println!("Worker {} got a job; executing.", id); 22 | 23 | job(); 24 | } 25 | Message::Terminate => { 26 | println!("Worker {} was told to terminate.", id); 27 | 28 | break; 29 | } 30 | } 31 | })), 32 | } 33 | } 34 | } 35 | 36 | pub type Job = Box; 37 | 38 | pub struct RequestFlow { 39 | pub request: Option, 40 | pub filename: String, 41 | pub status: String, 42 | } 43 | -------------------------------------------------------------------------------- /src/server/threadpool.rs: -------------------------------------------------------------------------------- 1 | use super::{message::Message, worker::Worker}; 2 | use std::sync::mpsc; 3 | use std::sync::Arc; 4 | use std::sync::Mutex; 5 | pub struct ThreadPool { 6 | workers: Vec, 7 | sender: mpsc::Sender, 8 | } 9 | 10 | impl ThreadPool { 11 | /// Create a new ThreadPool 12 | /// 13 | /// The size is the num of threads 14 | /// # Panics 15 | /// 16 | /// The `new` functions will panic if size is negative 17 | pub fn new(size: usize) -> ThreadPool { 18 | assert!(size > 0); 19 | 20 | let (sender, receiver) = mpsc::channel(); 21 | let receiver = Arc::new(Mutex::new(receiver)); 22 | 23 | let mut workers = Vec::with_capacity(size); 24 | 25 | for id in 0..size { 26 | workers.push(Worker::new(id, Arc::clone(&receiver))); 27 | } 28 | ThreadPool { workers, sender } 29 | } 30 | 31 | pub fn execute(&self, f: F) 32 | where 33 | F: FnOnce() + Send + 'static, 34 | { 35 | let job = Box::new(f); 36 | self.sender.send(Message::NewJob(job)).unwrap(); 37 | } 38 | } 39 | 40 | impl Drop for ThreadPool { 41 | fn drop(&mut self) { 42 | println!("Sending terminate message to all workers."); 43 | 44 | for _ in &self.workers { 45 | self.sender.send(Message::Terminate).unwrap(); 46 | } 47 | println!("Shutting down all workers"); 48 | for worker in &mut self.workers { 49 | println!("Shutting down {}", worker.id); 50 | 51 | if let Some(thread) = worker.thread.take() { 52 | thread.join().unwrap(); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | use pulldown_cmark::{html, Options, Parser}; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use server::response::Response; 6 | use server::threadpool::ThreadPool; 7 | use server::worker::RequestFlow; 8 | use std::env; 9 | use std::fs; 10 | use std::io::prelude::*; 11 | use std::io::Write; 12 | use std::net::TcpListener; 13 | use std::net::TcpStream; 14 | 15 | pub fn handle_connection(mut stream: TcpStream) { 16 | let mut buffer = [0; 1024]; 17 | stream.read(&mut buffer).unwrap(); 18 | 19 | let mapping = create_mapping(); 20 | 21 | let mut path = env::current_dir() 22 | .unwrap() 23 | .into_os_string() 24 | .into_string() 25 | .unwrap(); 26 | path.push_str("/templates/"); 27 | 28 | let mut status = ""; 29 | 30 | for val in mapping.iter() { 31 | if buffer.starts_with(val.request.clone().unwrap().as_bytes()) { 32 | status = &val.status; 33 | path.push_str(&val.filename); 34 | } 35 | } 36 | 37 | if status == "" { 38 | let unknown = unknown_request(); 39 | status = &unknown.status; 40 | path.push_str(&unknown.filename); 41 | 42 | create_response(&mut stream, path, status); 43 | } else { 44 | create_response(&mut stream, path, status); 45 | } 46 | } 47 | 48 | pub fn start_server(host: String, port: String, num_of_threads: usize) { 49 | let address: String = format!("{}:{}", host, port); 50 | let listener = TcpListener::bind(address).unwrap(); 51 | let pool = ThreadPool::new(num_of_threads); 52 | 53 | for stream in listener.incoming() { 54 | let stream = stream.unwrap(); 55 | 56 | pool.execute(|| { 57 | handle_connection(stream); 58 | }); 59 | } 60 | } 61 | 62 | pub fn create_mapping() -> Vec { 63 | // parse a json metadata 64 | let mut path = env::current_dir() 65 | .unwrap() 66 | .into_os_string() 67 | .into_string() 68 | .unwrap(); 69 | path.push_str("/metadata.json"); 70 | 71 | let json_data = fs::read_to_string(path).unwrap(); 72 | let parsed_data: JsonSerialize = 73 | serde_json::from_str(&json_data).expect("JSON was not well-formatted"); 74 | 75 | let mut mapping: Vec = Vec::new(); 76 | 77 | for val in parsed_data.endpoints { 78 | let request = String::from(val.method); 79 | let path = val.endpoint.as_str(); 80 | let filename = val.template; 81 | 82 | mapping.push(RequestFlow { 83 | request: Some(String::from(format!("{} {} HTTP/1.1\r\n", request, path))), 84 | filename: filename, 85 | status: String::from("HTTP/1.1 200 OK"), 86 | }) 87 | } 88 | 89 | mapping 90 | } 91 | 92 | pub fn unknown_request() -> RequestFlow { 93 | RequestFlow { 94 | request: None, 95 | filename: String::from("404.html"), 96 | status: String::from("HTTP/1.1 404 NOT FOUND"), 97 | } 98 | } 99 | 100 | pub fn create_response(stream: &mut TcpStream, path: String, status: &str) { 101 | let contents = fs::read_to_string(path).unwrap(); 102 | 103 | let mut options = Options::empty(); 104 | options.insert(Options::ENABLE_STRIKETHROUGH); 105 | let parser = Parser::new_ext(&contents, options); 106 | 107 | // Write to String buffer. 108 | let mut html_output = String::new(); 109 | html::push_html(&mut html_output, parser); 110 | 111 | let response = Response::new(status, html_output); 112 | stream.write(response.body.as_bytes()).unwrap(); 113 | stream.flush().unwrap(); 114 | } 115 | 116 | #[derive(Serialize, Deserialize, Debug)] 117 | struct JsonItem { 118 | method: String, 119 | endpoint: String, 120 | template: String, 121 | } 122 | #[derive(Serialize, Deserialize, Debug)] 123 | struct JsonSerialize { 124 | endpoints: Vec, 125 | } 126 | --------------------------------------------------------------------------------