diff --git a/Cargo.lock b/Cargo.lock index d4b1108..9447b04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "hteapot" -version = "0.2.6" +version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index df36524..a6e3813 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,7 @@ [package] name = "hteapot" -version = "0.2.6" -exclude = [ - "hteapot.toml", - "public/", - "readme.md" -] +version = "0.3.0" +exclude = ["hteapot.toml", "public/", "readme.md"] license = "MIT" keywords = ["HTTP", "HTTP-SERVER"] description = "HTeaPot is a lightweight HTTP server library designed to be easy to use and extend." @@ -17,9 +13,7 @@ authors = ["Alb Ruiz G. "] [lib] name = "hteapot" -path = "src/hteapot.rs" +path = "src/hteapot/mod.rs" [[bin]] name = "hteapot" - - diff --git a/config.toml b/config.toml index d29fdb4..41a4390 100644 --- a/config.toml +++ b/config.toml @@ -1,11 +1,12 @@ [HTEAPOT] port = 8081 host = "0.0.0.0" +threads = 4 root = "public" -threads = 5 cache = true -cache_ttl = 3600 +cache_ttl = 36 [proxy] "/test" = "http://example.com" "/google" = "http://google.com" +"/myip" = "http://ifconfig.co" # "/" = "http://ifconfig.co" # this will override all the proxys and local request diff --git a/readme.md b/readme.md index fe908f9..9e57555 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,9 @@ [Spanish](docs/readme-es.md) | English -HteaPot is a simple HTTP server written in Rust. It allows you to serve static files and handle basic HTTP requests. +HteaPot is a simple HTTP server written in Rust. It allows you to serve static files and handle +HTTP requests. +It´s also a library to write http applictions like an api # Features @@ -33,13 +35,33 @@ You can configure the server using a TOML file. Here's an example configuration: ```toml [HTEAPOT] port = 8081 # The port on which the server will listen for incoming connections. -host = "localhost" # The host address to bind the server to. +host = "localhost" # The host address to bind the server to. root = "public" # The root directory from which to serve files. ``` + +# Library use + +For use hteapot as a library in rust + 1. Install the library + ```bash + $ cargo add hteapot + ``` + + 2. Then you can use it in your project +```Rust +use hteapot::{HttpStatus, Hteapot}; + +fn main() { + let server = Hteapot::new("localhost", 8081); + teapot.listen(move|req| { +} +``` + + # Contributing Contributions are welcome! Feel free to open issues or submit pull requests. # License -This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/src/brew.rs b/src/brew.rs index 87c0965..e351046 100644 --- a/src/brew.rs +++ b/src/brew.rs @@ -6,6 +6,7 @@ use std::{ net::TcpStream, }; +#[derive(Debug)] struct Url { scheme: String, domain: String, @@ -27,7 +28,11 @@ fn parse_url(url: &str) -> Result { _ => "80", } }; - let (domain, path) = domain_path.split_once('/').unwrap(); + let (domain, path) = match domain_path.split_once('/') { + Some((a, b)) => (a, b), + None => (domain_path, ""), + }; + Ok(Url { scheme: prefix.to_string(), domain: domain.to_string(), @@ -51,21 +56,19 @@ pub fn fetch(url: &str) -> Result, &str> { return Err("Error fetching"); } let mut client = client.unwrap(); - let http_request = format!("GET /{} HTTP/1.1\r\nHost: {}\r\n\r\n", url.path, url.domain); + let http_request = format!( + "GET /{} HTTP/1.1\nHost: {}\nConnection: Close\n\n", + url.path, url.domain + ); client.write(http_request.as_bytes()).unwrap(); + let _ = client.flush(); let mut full_buffer: Vec = Vec::new(); let mut buffer = [0; 1024]; loop { match client.read(&mut buffer) { Ok(0) => break, - Ok(n) => { - if n == 0 { - break; - } + Ok(_n) => { full_buffer.extend(buffer.iter().cloned()); - if buffer.last().unwrap() == &0 { - break; - } } Err(_) => break, } diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..53107bc --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,59 @@ +// Written by Alberto Ruiz, 2024-11-05 +// Config module: handles application configuration setup and parsing. +// This module defines structs and functions to load and validate +// configuration settings from files, environment variables, or other sources. +use std::collections::HashMap; +use std::time; +use std::time::SystemTime; + +pub struct Cache { + //TODO: consider make it generic + data: HashMap, u64)>, + max_ttl: u64, +} + +impl Cache { + pub fn new(max_ttl: u64) -> Self { + Cache { + data: HashMap::new(), + max_ttl, + } + } + + fn validate_ttl(&self, ttl: u64) -> bool { + let now = SystemTime::now(); + let since_epoch = now + .duration_since(time::UNIX_EPOCH) + .expect("Time went backwards"); + let secs = since_epoch.as_secs(); + secs < ttl + } + + fn get_ttl(&self) -> u64 { + let now = SystemTime::now(); + let since_epoch = now + .duration_since(time::UNIX_EPOCH) + .expect("Time went backwards"); + let secs = since_epoch.as_secs(); + secs + self.max_ttl + } + + pub fn set(&mut self, key: String, data: Vec) { + self.data.insert(key, (data, self.get_ttl())); + } + + pub fn get(&mut self, key: String) -> Option> { + let r = self.data.get(&key); + if r.is_some() { + let (data, ttl) = r.unwrap(); + if self.validate_ttl(*ttl) { + Some(data.clone()) + } else { + self.data.remove(&key); + None + } + } else { + None + } + } +} diff --git a/src/config.rs b/src/config.rs index 8224478..37d934f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -96,12 +96,13 @@ pub fn toml_parser(content: &str) -> HashMap { map } +#[derive(Debug)] pub struct Config { pub port: u16, // Port number to listen pub host: String, // Host name or IP pub root: String, // Root directory to serve files pub cache: bool, - pub cache_ttl: u64, + pub cache_ttl: u16, pub threads: u16, pub index: String, // Index file to serve by default //pub error: String, // Error file to serve when a file is not found diff --git a/src/hteapot/methods.rs b/src/hteapot/methods.rs new file mode 100644 index 0000000..43b1efc --- /dev/null +++ b/src/hteapot/methods.rs @@ -0,0 +1,50 @@ +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum HttpMethod { + GET, + POST, + PUT, + DELETE, + PATCH, + HEAD, + OPTIONS, + TRACE, + CONNECT, + Other(String), +} + +impl HttpMethod { + pub fn from_str(method: &str) -> HttpMethod { + match method { + "GET" => HttpMethod::GET, + "POST" => HttpMethod::POST, + "PUT" => HttpMethod::PUT, + "DELETE" => HttpMethod::DELETE, + "PATCH" => HttpMethod::PATCH, + "HEAD" => HttpMethod::HEAD, + "OPTIONS" => HttpMethod::OPTIONS, + "TRACE" => HttpMethod::TRACE, + "CONNECT" => HttpMethod::CONNECT, + _ => Self::Other(method.to_string()), + } + } + pub fn to_str(&self) -> &str { + match self { + HttpMethod::GET => "GET", + HttpMethod::POST => "POST", + HttpMethod::PUT => "PUT", + HttpMethod::DELETE => "DELETE", + HttpMethod::PATCH => "PATCH", + HttpMethod::HEAD => "HEAD", + HttpMethod::OPTIONS => "OPTIONS", + HttpMethod::TRACE => "TRACE", + HttpMethod::CONNECT => "CONNECT", + HttpMethod::Other(method) => method.as_str(), + } + } +} + +// #[derive(Clone, Copy)] +// pub enum Protocol { +// HTTP, +// HTTPS, +// } diff --git a/src/hteapot.rs b/src/hteapot/mod.rs similarity index 60% rename from src/hteapot.rs rename to src/hteapot/mod.rs index 0b37deb..a7a0918 100644 --- a/src/hteapot.rs +++ b/src/hteapot/mod.rs @@ -2,60 +2,23 @@ // This is the HTTP server module, it will handle the requests and responses // Also provide utilities to parse the requests and build the responses +mod methods; +mod response; +mod status; + +pub use self::methods::HttpMethod; +pub use self::response::HttpResponse; +pub use self::status::HttpStatus; + use std::collections::{HashMap, VecDeque}; -use std::hash::Hash; use std::io::{self, BufReader, BufWriter, Read, Write}; use std::net::{Shutdown, TcpListener, TcpStream}; use std::sync::{Arc, Condvar, Mutex}; use std::thread; +use std::time::Duration; const VERSION: &str = env!("CARGO_PKG_VERSION"); -#[derive(Debug, PartialEq, Eq, Hash)] -pub enum HttpMethod { - GET, - POST, - PUT, - DELETE, - PATCH, - HEAD, - OPTIONS, - TRACE, - CONNECT, - Other(String), -} - -impl HttpMethod { - pub fn from_str(method: &str) -> HttpMethod { - match method { - "GET" => HttpMethod::GET, - "POST" => HttpMethod::POST, - "PUT" => HttpMethod::PUT, - "DELETE" => HttpMethod::DELETE, - "PATCH" => HttpMethod::PATCH, - "HEAD" => HttpMethod::HEAD, - "OPTIONS" => HttpMethod::OPTIONS, - "TRACE" => HttpMethod::TRACE, - "CONNECT" => HttpMethod::CONNECT, - _ => Self::Other(method.to_string()), - } - } - pub fn to_str(&self) -> &str { - match self { - HttpMethod::GET => "GET", - HttpMethod::POST => "POST", - HttpMethod::PUT => "PUT", - HttpMethod::DELETE => "DELETE", - HttpMethod::PATCH => "PATCH", - HttpMethod::HEAD => "HEAD", - HttpMethod::OPTIONS => "OPTIONS", - HttpMethod::TRACE => "TRACE", - HttpMethod::CONNECT => "CONNECT", - HttpMethod::Other(method) => method.as_str(), - } - } -} - #[macro_export] macro_rules! headers { ( $($k:expr => $v:expr),*) => { @@ -68,77 +31,6 @@ macro_rules! headers { }; } -#[derive(Clone, Copy)] -pub enum Protocol { - HTTP, - HTTPS, -} - -#[derive(Clone, Copy)] -pub enum HttpStatus { - OK = 200, - Created = 201, - Accepted = 202, - NoContent = 204, - MovedPermanently = 301, - MovedTemporarily = 302, - NotModified = 304, - BadRequest = 400, - Unauthorized = 401, - Forbidden = 403, - NotFound = 404, - IAmATeapot = 418, - InternalServerError = 500, - NotImplemented = 501, - BadGateway = 502, - ServiceUnavailable = 503, -} - -impl HttpStatus { - pub fn from_u16(status: u16) -> HttpStatus { - match status { - 200 => HttpStatus::OK, - 201 => HttpStatus::Created, - 202 => HttpStatus::Accepted, - 204 => HttpStatus::NoContent, - 301 => HttpStatus::MovedPermanently, - 302 => HttpStatus::MovedTemporarily, - 304 => HttpStatus::NotModified, - 400 => HttpStatus::BadRequest, - 401 => HttpStatus::Unauthorized, - 403 => HttpStatus::Forbidden, - 404 => HttpStatus::NotFound, - 418 => HttpStatus::IAmATeapot, - 500 => HttpStatus::InternalServerError, - 501 => HttpStatus::NotImplemented, - 502 => HttpStatus::BadGateway, - 503 => HttpStatus::ServiceUnavailable, - _ => panic!("Invalid HTTP status"), - } - } - - fn to_string(&self) -> &str { - match self { - HttpStatus::OK => "OK", - HttpStatus::Created => "Created", - HttpStatus::Accepted => "Accepted", - HttpStatus::NoContent => "No Content", - HttpStatus::MovedPermanently => "Moved Permanently", - HttpStatus::MovedTemporarily => "Moved Temporarily", - HttpStatus::NotModified => "Not Modified", - HttpStatus::BadRequest => "Bad Request", - HttpStatus::Unauthorized => "Unauthorized", - HttpStatus::Forbidden => "Forbidden", - HttpStatus::NotFound => "Not Found", - HttpStatus::IAmATeapot => "I'm a teapot", - HttpStatus::InternalServerError => "Internal Server Error", - HttpStatus::NotImplemented => "Not Implemented", - HttpStatus::BadGateway => "Bad Gateway", - HttpStatus::ServiceUnavailable => "Service Unavailable", - } - } -} - pub struct HttpRequest { pub method: HttpMethod, pub path: String, @@ -151,40 +43,44 @@ pub struct Hteapot { port: u16, address: String, threads: u16, - //cache: HashMap, - //pool: Arc<(Mutex>, Condvar)>, } #[derive(Clone, Debug)] struct SocketStatus { + // TODO: write proper ttl reading: bool, data_readed: Vec, data_write: Vec, index_writed: usize, } +struct SocketData { + stream: TcpStream, + status: Option, +} + impl Hteapot { // Constructor pub fn new(address: &str, port: u16) -> Self { Hteapot { - port: port, + port, address: address.to_string(), threads: 1, //cache: HashMap::new(), } } - pub fn new_threaded(address: &str, port: u16, thread: u16) -> Self { + pub fn new_threaded(address: &str, port: u16, threads: u16) -> Self { Hteapot { - port: port, + port, address: address.to_string(), - threads: thread, + threads: if threads == 0 { 1 } else { threads }, //cache: HashMap::new(), } } // Start the server - pub fn listen(&self, action: impl Fn(HttpRequest) -> Vec + Send + Sync + 'static) { + pub fn listen(&self, action: impl Fn(HttpRequest) -> HttpResponse + Send + Sync + 'static) { let addr = format!("{}:{}", self.address, self.port); let listener = TcpListener::bind(addr); let listener = match listener { @@ -197,54 +93,84 @@ impl Hteapot { let pool: Arc<(Mutex>, Condvar)> = Arc::new((Mutex::new(VecDeque::new()), Condvar::new())); //let statusPool = Arc::new(Mutex::new(HashMap::::new())); + let priority_list: Arc>> = Arc::new(Mutex::new(Vec::new())); let arc_action = Arc::new(action); - for _tn in 0..self.threads { + let _tn = _tn as usize; let pool_clone = pool.clone(); let action_clone = arc_action.clone(); + let pl_clone = priority_list.clone(); + { + let mut pl_lock = pl_clone.lock().expect("Error locking prority list"); + pl_lock.push(0); + } thread::spawn(move || { let mut streams_to_handle = Vec::new(); - let mut streams_data: HashMap = HashMap::new(); loop { { let (lock, cvar) = &*pool_clone; let mut pool = lock.lock().expect("Error locking pool"); + let pl_copy; + { + let pl_lock = pl_clone.lock().expect("Error locking prority list"); + pl_copy = pl_lock.clone(); + } + if streams_to_handle.is_empty() { pool = cvar .wait_while(pool, |pool| pool.is_empty()) .expect("Error waiting on cvar"); + } else if pl_copy.len() != 1 + && streams_to_handle.len() < 10 + && pl_copy + .iter() + .find(|&&v| streams_to_handle.len() > v) + .is_none() + { + (pool, _) = cvar + .wait_timeout_while(pool, Duration::from_millis(500), |pool| { + pool.is_empty() + }) + .expect("Error waiting on cvar"); } if !pool.is_empty() { - streams_to_handle.push(pool.pop_back().unwrap()); - } - } - - streams_to_handle.retain(|stream| { - //println!("Handling request by {}", tn); - let local_addr = stream.local_addr().unwrap().to_string(); - let action_clone = action_clone.clone(); - let status = match streams_data.get(&local_addr) { - Some(d) => d.clone(), - None => SocketStatus { + let socket_status = SocketStatus { reading: true, data_readed: vec![], data_write: vec![], index_writed: 0, - }, - }; + }; + let socket_data = SocketData { + stream: pool.pop_back().unwrap(), + status: Some(socket_status), + }; + streams_to_handle.push(socket_data); - let r = Hteapot::handle_client(stream, status, move |request| { - action_clone(request) - }); - if r.is_some() { - streams_data.insert(local_addr, r.unwrap()); - return true; - } else { - streams_data.remove(&local_addr); - return false; + { + let mut pl_lock = + pl_clone.lock().expect("Errpr locking prority list"); + pl_lock[_tn] = streams_to_handle.len(); + } } - }); + } + + for stream_data in streams_to_handle.iter_mut() { + if stream_data.status.is_none() { + continue; + } + let r = Hteapot::handle_client( + &stream_data.stream, + stream_data.status.as_mut().unwrap().clone(), + &action_clone, + ); + stream_data.status = r; + } + streams_to_handle.retain(|s| s.status.is_some()); + { + let mut pl_lock = pl_clone.lock().expect("Errpr locking prority list"); + pl_lock[_tn] = streams_to_handle.len(); + } } }); } @@ -259,7 +185,7 @@ impl Hteapot { stream .set_nonblocking(true) .expect("Error seting non blocking"); - //stream.set_nodelay(true).expect("Error seting no delay"); + stream.set_nodelay(true).expect("Error seting no delay"); { let (lock, cvar) = &*pool_clone; let mut pool = lock.lock().expect("Error locking pool"); @@ -271,45 +197,12 @@ impl Hteapot { } } - // Create a response - pub fn response_maker>( - status: HttpStatus, - content: B, - headers: Option>, - ) -> Vec { - let content = content.as_ref(); - let status_text = status.to_string(); - let mut headers_text = String::new(); - let mut headers = if headers.is_some() { - headers.unwrap() - } else { - HashMap::new() - }; - headers.insert("Content-Length".to_string(), content.len().to_string()); - headers.insert( - "Server".to_string(), - format!("HTeaPot/{}", VERSION).to_string(), - ); - for (key, value) in headers.iter() { - headers_text.push_str(&format!("{}: {}\r\n", key, value)); - } - let response_header = format!( - "HTTP/1.1 {} {}\r\n{}\r\n", - status as u16, status_text, headers_text - ); - let mut response = Vec::new(); - response.extend_from_slice(response_header.as_bytes()); - response.extend_from_slice(content); - response.push(0x0D); // Carriage Return - response.push(0x0A); // Line Feed - response - } - // Parse the request pub fn request_parser(request: String) -> Result { let mut lines = request.lines(); let first_line = lines.next(); if first_line.is_none() { + println!("{}", request); return Err("Invalid request".to_string()); } let first_line = first_line.unwrap(); @@ -379,8 +272,8 @@ impl Hteapot { Ok(HttpRequest { method: HttpMethod::from_str(method), path: path.to_string(), - args: args, - headers: headers, + args, + headers, body: body.trim_end().to_string(), }) } @@ -389,7 +282,7 @@ impl Hteapot { fn handle_client( stream: &TcpStream, socket_status: SocketStatus, - action: impl Fn(HttpRequest) -> Vec + Send + Sync + 'static, + action: &Arc HttpResponse + Send + Sync + 'static>, ) -> Option { let mut reader = BufReader::new(stream); let mut writer = BufWriter::new(stream); @@ -402,6 +295,9 @@ impl Hteapot { io::ErrorKind::WouldBlock => { return Some(socket_status); } + io::ErrorKind::ConnectionReset => { + return None; + } _ => { println!("R Error{:?}", e); return None; @@ -409,7 +305,7 @@ impl Hteapot { }, Ok(m) => { if m == 0 { - break; + return None; } } }; @@ -433,8 +329,22 @@ impl Hteapot { return None; } let request = request.unwrap(); + let keep_alive = match request.headers.get("Connection") { + Some(ch) => ch == "keep-alive", + None => false, + }; if socket_status.data_write.len() == 0 { - socket_status.data_write = action(request); + let mut response = action(request); + if !response.headers.contains_key("Conection") && keep_alive { + response + .headers + .insert("Connection".to_string(), "keep_alive".to_string()); + } else { + response + .headers + .insert("Connection".to_string(), "close".to_string()); + } + socket_status.data_write = response.to_bytes(); } for n in socket_status.index_writed..socket_status.data_write.len() { let r = writer.write(&[socket_status.data_write[n]]); @@ -455,8 +365,16 @@ impl Hteapot { eprintln!("Error2: {}", r.err().unwrap()); return Some(socket_status); } - let _ = stream.shutdown(Shutdown::Both); - None + if keep_alive { + socket_status.reading = true; + socket_status.data_readed = vec![]; + socket_status.data_write = vec![]; + socket_status.index_writed = 0; + return Some(socket_status); + } else { + let _ = stream.shutdown(Shutdown::Both); + None + } } } @@ -475,8 +393,8 @@ fn test_http_parser() { #[test] fn test_http_response_maker() { - let response = Hteapot::response_maker(HttpStatus::IAmATeapot, "Hello, World!", None); - let response = String::from_utf8(response).unwrap(); + let response = HttpResponse::new(HttpStatus::IAmATeapot, "Hello, World!", None); + let response = String::from_utf8(response.to_bytes()).unwrap(); let expected_response = format!("HTTP/1.1 418 I'm a teapot\r\nContent-Length: 13\r\nServer: HTeaPot/{}\r\n\r\nHello, World!\r\n",VERSION); let expected_response_list = expected_response.split("\r\n"); for item in expected_response_list.into_iter() { diff --git a/src/hteapot/response.rs b/src/hteapot/response.rs new file mode 100644 index 0000000..8dc8cd1 --- /dev/null +++ b/src/hteapot/response.rs @@ -0,0 +1,70 @@ +use super::HttpStatus; +use super::VERSION; +use std::collections::HashMap; + +pub struct HttpResponse { + pub status: HttpStatus, + pub headers: HashMap, + pub content: Vec, + raw: Option>, + is_raw: bool, +} + +impl HttpResponse { + pub fn new>( + status: HttpStatus, + content: B, + headers: Option>, + ) -> Self { + let mut headers = headers.unwrap_or(HashMap::new()); + let content = content.as_ref(); + headers.insert("Content-Length".to_string(), content.len().to_string()); + headers.insert( + "Server".to_string(), + format!("HTeaPot/{}", VERSION).to_string(), + ); + HttpResponse { + status, + headers, + content: content.to_owned(), + raw: None, + is_raw: false, + } + } + + pub fn new_raw(raw: Vec) -> Self { + HttpResponse { + status: HttpStatus::IAmATeapot, + headers: HashMap::new(), + content: vec![], + raw: Some(raw), + is_raw: true, + } + } + + pub fn is_raw(&self) -> bool { + self.is_raw + } + + pub fn to_bytes(&self) -> Vec { + if self.is_raw() { + return self.raw.clone().unwrap(); + } + let mut headers_text = String::new(); + for (key, value) in self.headers.iter() { + headers_text.push_str(&format!("{}: {}\r\n", key, value)); + } + let response_header = format!( + "HTTP/1.1 {} {}\r\n{}\r\n", + self.status as u16, + self.status.to_string(), + headers_text + ); + let mut response = Vec::new(); + response.extend_from_slice(response_header.as_bytes()); + response.append(&mut self.content.clone()); + response.push(0x0D); // Carriage Return + response.push(0x0A); // Line Feed + response + } +} diff --git a/src/hteapot/status.rs b/src/hteapot/status.rs new file mode 100644 index 0000000..0db0cda --- /dev/null +++ b/src/hteapot/status.rs @@ -0,0 +1,64 @@ +#[derive(Clone, Copy)] +pub enum HttpStatus { + OK = 200, + Created = 201, + Accepted = 202, + NoContent = 204, + MovedPermanently = 301, + MovedTemporarily = 302, + NotModified = 304, + BadRequest = 400, + Unauthorized = 401, + Forbidden = 403, + NotFound = 404, + IAmATeapot = 418, + InternalServerError = 500, + NotImplemented = 501, + BadGateway = 502, + ServiceUnavailable = 503, +} + +impl HttpStatus { + pub fn from_u16(status: u16) -> HttpStatus { + match status { + 200 => HttpStatus::OK, + 201 => HttpStatus::Created, + 202 => HttpStatus::Accepted, + 204 => HttpStatus::NoContent, + 301 => HttpStatus::MovedPermanently, + 302 => HttpStatus::MovedTemporarily, + 304 => HttpStatus::NotModified, + 400 => HttpStatus::BadRequest, + 401 => HttpStatus::Unauthorized, + 403 => HttpStatus::Forbidden, + 404 => HttpStatus::NotFound, + 418 => HttpStatus::IAmATeapot, + 500 => HttpStatus::InternalServerError, + 501 => HttpStatus::NotImplemented, + 502 => HttpStatus::BadGateway, + 503 => HttpStatus::ServiceUnavailable, + _ => panic!("Invalid HTTP status"), + } + } + + pub fn to_string(&self) -> &str { + match self { + HttpStatus::OK => "OK", + HttpStatus::Created => "Created", + HttpStatus::Accepted => "Accepted", + HttpStatus::NoContent => "No Content", + HttpStatus::MovedPermanently => "Moved Permanently", + HttpStatus::MovedTemporarily => "Moved Temporarily", + HttpStatus::NotModified => "Not Modified", + HttpStatus::BadRequest => "Bad Request", + HttpStatus::Unauthorized => "Unauthorized", + HttpStatus::Forbidden => "Forbidden", + HttpStatus::NotFound => "Not Found", + HttpStatus::IAmATeapot => "I'm a teapot", + HttpStatus::InternalServerError => "Internal Server Error", + HttpStatus::NotImplemented => "Not Implemented", + HttpStatus::BadGateway => "Bad Gateway", + HttpStatus::ServiceUnavailable => "Service Unavailable", + } + } +} diff --git a/src/main.rs b/src/main.rs index 6016f0a..1909fda 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,43 +1,138 @@ mod brew; +mod cache; mod config; pub mod hteapot; mod logger; -use std::collections::HashMap; use std::fs; use std::io; +use std::path::Path; use std::sync::Mutex; -use std::time; -use std::time::SystemTime; use brew::fetch; -use hteapot::Hteapot; +use cache::Cache; +use config::Config; +use hteapot::{Hteapot, HttpResponse, HttpStatus}; -use hteapot::HttpStatus; use logger::Logger; const VERSION: &str = env!("CARGO_PKG_VERSION"); +fn is_proxy(config: &Config, path: String) -> Option { + for proxy_path in config.proxy_rules.keys() { + let path_proxy = path.strip_prefix(proxy_path); + if path_proxy.is_some() { + let path_proxy = path_proxy.unwrap(); + let url = config.proxy_rules.get(proxy_path).unwrap(); + let separator = if path_proxy.starts_with('/') || url.ends_with('/') { + "" + } else { + "/" + }; + let url = format!("{}{}{}", url, separator, path_proxy); + return Some(url); + } + } + None +} + +fn serve_proxy(proxy_url: String) -> HttpResponse { + let raw_response = fetch(&proxy_url); + match raw_response { + Ok(raw) => HttpResponse::new_raw(raw), + Err(_) => HttpResponse::new(HttpStatus::NotFound, "not found", None), + } +} + +fn get_mime_tipe(config: &Config, path: String) -> String { + let mut path = format!("{}{}", config.root, path); + if Path::new(path.as_str()).is_dir() { + let separator = if path.ends_with('/') { "" } else { "/" }; + path = format!("{}{}{}", path, separator, config.index); + } + let extension = Path::new(path.as_str()) + .extension() + .unwrap() + .to_str() + .unwrap(); + let mimetipe = match extension { + "js" => "text/javascript", + "json" => "application/json", + "css" => "text/css", + "html" => "text/html", + _ => "text/plain", + }; + + mimetipe.to_string() +} + +fn serve_file(config: &Config, path: String) -> Option> { + let mut path = format!("{}{}", config.root, path); + if Path::new(path.as_str()).is_dir() { + let separator = if path.ends_with('/') { "" } else { "/" }; + path = format!("{}{}{}", path, separator, config.index); + } + let r = fs::read(path); + if r.is_ok() { + Some(r.unwrap()) + } else { + None + } +} + fn main() { let args = std::env::args().collect::>(); - if args[1] == "--version" || args[1] == "-v" { - println!("Hteapot {}", VERSION); - return; - } - if args[1] == "--help" || args[1] == "-h" { - println!("Hteapot {}", VERSION); - println!("usage: {} ", args[0]); - return; + let mut serving_path = None; + if args.len() >= 2 { + match args[1].as_str() { + "--help" | "-h" => { + println!("Hteapot {}", VERSION); + println!("usage: {} ", args[0]); + return; + } + "--version" | "-v" => { + println!("Hteapot {}", VERSION); + return; + } + "--serve" | "-s" => { + serving_path = Some(args.get(2).unwrap().clone()); + } + _ => (), + }; } - let config = if args.len() > 1 { + let config = if args.len() == 2 { config::Config::load_config(&args[1]) + } else if serving_path.is_some() { + let serving_path_str = serving_path.unwrap(); + let serving_path_str = serving_path_str.as_str(); + let serving_path = Path::new(serving_path_str); + let mut c = config::Config::new_default(); + c.host = "0.0.0.0".to_string(); + if serving_path.is_dir() { + c.root = serving_path.to_str().unwrap_or_default().to_string(); + } else { + c.index = serving_path + .file_name() + .unwrap() + .to_str() + .unwrap_or_default() + .to_string(); + c.root = serving_path + .parent() + .unwrap_or(Path::new("./")) + .to_str() + .unwrap_or_default() + .to_string(); + } + c } else { config::Config::new_default() }; + let proxy_only = config.proxy_rules.get("/").is_some(); let logger = Mutex::new(Logger::new(io::stdout())); - let cache: Mutex, u64)>> = Mutex::new(HashMap::new()); + let cache: Mutex = Mutex::new(Cache::new(config.cache_ttl as u64)); let server = Hteapot::new_threaded(config.host.as_str(), config.port, config.threads); logger.lock().expect("this doesnt work :C").msg(format!( "Server started at http://{}:{}", @@ -57,122 +152,40 @@ fn main() { } server.listen(move |req| { - //let mut logger = Logger::new(io::stdout()); + // SERVER CORE + // for each request + logger.lock().expect("this doesnt work :C").msg(format!( "Request {} {}", req.method.to_str(), req.path )); - let path = if req.path.ends_with("/") { - let mut path = req.path.clone(); - path.push_str(&config.index); - path - } else { - req.path.clone() - }; - let path_clone = req.path.clone(); - let divided_path: Vec<&str> = path_clone.split('/').skip(1).collect(); - if divided_path.is_empty() { - return Hteapot::response_maker(HttpStatus::BadRequest, b"Invalid path", None); - } - let first_one = format!("/{}", divided_path[0]); - let rest_path = divided_path[1..].join("/"); + let is_proxy = is_proxy(&config, req.path.clone()); - if proxy_only || config.proxy_rules.contains_key(&first_one) { - let url = if proxy_only { - let url = config.proxy_rules.get("/").unwrap(); - if rest_path.len() != 0 { - format!("{}{}/{}", url, first_one, rest_path) - } else { - format!("{}{}", url, first_one) - } - } else { - let url = config.proxy_rules.get(&first_one).unwrap(); - format!("{}/{}", url, rest_path) - }; - logger - .lock() - .expect("this doesnt work :C") - .msg(format!("Proxying to: {}", url)); - return match fetch(&url) { - Ok(response) => response, - Err(err) => { - Hteapot::response_maker(HttpStatus::InternalServerError, err.as_bytes(), None) - } - }; + if proxy_only || is_proxy.is_some() { + return serve_proxy(is_proxy.unwrap()); + } + if !Path::new(req.path.clone().as_str()).exists() { + return HttpResponse::new(HttpStatus::NotFound, "Not found", None); } - let path = format!("./{}/{}", config.root, path); - let cache_result = { - if config.cache { - let cache = cache.lock(); - if cache.is_err() { - None - } else { - let cache = cache.unwrap(); - let r = cache.get(&path); - match r { - Some(r) => Some(r.clone()), - None => None, - } + let mimetype = get_mime_tipe(&config, req.path.clone()); + let content: Option> = if config.cache { + let mut cachee = cache.lock().expect("Error locking cache"); + let mut r = cachee.get(req.path.clone()); + if r.is_none() { + r = serve_file(&config, req.path.clone()); + if r.is_some() { + cachee.set(req.path.clone(), r.clone().unwrap()); } - } else { - None - } - }; - let mut is_cache = false; - let content: Result, _> = if cache_result.is_some() { - let (content, ttl) = cache_result.unwrap(); - let now = SystemTime::now(); - let since_epoch = now - .duration_since(time::UNIX_EPOCH) - .expect("Time went backwards"); - let secs = since_epoch.as_secs(); - if secs > ttl { - fs::read(&path) - } else { - is_cache = true; - Ok(content) } + r } else { - fs::read(&path) + serve_file(&config, req.path) }; match content { - Ok(content) => { - if config.cache { - let cache = cache.lock(); - if cache.is_ok() && is_cache { - let mut cache = cache.unwrap(); - let now = SystemTime::now(); - let since_epoch = now - .duration_since(time::UNIX_EPOCH) - .expect("Time went backwards"); - let secs = since_epoch.as_secs() + config.cache_ttl; - cache.insert(path, (content.clone(), secs)); - } - } - return Hteapot::response_maker( - HttpStatus::OK, - &content, - headers!("Connection" => "close"), - ); - } - Err(e) => match e.kind() { - io::ErrorKind::NotFound => { - return Hteapot::response_maker( - HttpStatus::NotFound, - "

404 Not Found

", - headers!("Content-Type" => "text/html", "Server" => "HteaPot"), - ); - } - _ => { - return Hteapot::response_maker( - HttpStatus::InternalServerError, - "

500 Internal Server Error

", - headers!("Content-Type" => "text/html", "Server" => "HteaPot"), - ); - } - }, + Some(c) => HttpResponse::new(HttpStatus::OK, c, headers!("Content-Type" => mimetype)), + None => HttpResponse::new(HttpStatus::NotFound, "Not found", None), } }); }