├── 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 |
--------------------------------------------------------------------------------