diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/hteapot.toml b/hteapot.toml index 8641916..bec18e8 100644 --- a/hteapot.toml +++ b/hteapot.toml @@ -2,6 +2,8 @@ port = 8081 host = "0.0.0.0" #test de comentario root = "public" +threads = 5 +cache = true [proxy] "/test" = "https://example.com" "/google" = "http://google.com" \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index e009a59..d63c5d2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,10 +2,40 @@ // This is the config module, it will load the configuration // file and provide the settings -use std::{collections::HashMap, fs}; +use std::{any::Any, collections::HashMap, fs}; + +#[derive(Clone)] +#[derive(Debug)] +pub enum TOMLtype { + Text(String), + Number(u16), + Float(f64), + Boolean(bool), +} + +type TOMLSchema = HashMap; +trait Schema { + fn get2(&self, key: &str) -> Option ; +} -pub fn toml_parser(content: &str) -> HashMap> { +impl Schema for TOMLSchema { + fn get2(&self, key: &str) -> Option { + let value = self.get(key)?; + let value = value.clone(); + let any_value: Box = match value { + TOMLtype::Text(d) => Box::new(d), + TOMLtype::Number(d) => Box::new(d), + TOMLtype::Float(d) => Box::new(d), + TOMLtype::Boolean(d) => Box::new(d) + }; + let r = any_value.downcast_ref::().cloned(); + if r.is_none() {println!("{} is none", key);} + r + } +} + +pub fn toml_parser(content: &str) -> HashMap { let mut map = HashMap::new(); let mut submap = HashMap::new(); let mut title = "".to_string(); @@ -34,13 +64,31 @@ pub fn toml_parser(content: &str) -> HashMap> { continue; } let key = parts[0].trim().trim_end_matches('"').trim_start_matches('"'); - println!("{}",key); if key.is_empty(){ continue; } let value = parts[1].trim(); - let value = value.trim_matches('"').trim(); - submap.insert(key.to_string(), value.to_string()); + let value = if value.contains('\'') || value.contains('"') { + let value = value.trim_matches('"').trim(); + TOMLtype::Text(value.to_string()) + } else if value.to_lowercase() == "true" || value.to_lowercase() == "false" { + let value = value.to_lowercase() == "true"; + TOMLtype::Boolean(value) + } else if value.contains('.') { + let value = value.parse::(); + if value.is_err() { + panic!("Error parsing toml"); + } + TOMLtype::Float(value.unwrap()) + } else { + let value = value.parse::(); + if value.is_err() { + panic!("Error parsing toml"); + } + TOMLtype::Number(value.unwrap()) + + }; + submap.insert(key.to_string(), value); } map.insert(title, submap.clone()); map @@ -52,6 +100,8 @@ pub struct Config { pub host: String, // Host name or IP pub root: String, // Root directory to serve files pub cache: bool, + pub cache_ttl: u64, + 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 pub proxy_rules: HashMap @@ -76,7 +126,9 @@ impl Config { root: "./".to_string(), index: "index.html".to_string(), error: "error.html".to_string(), + threads: 1, cache: false, + cache_ttl: 0, proxy_rules: HashMap::new() } } @@ -88,27 +140,31 @@ impl Config { } let content = content.unwrap(); let map = toml_parser(&content); - println!("{:?}", map); - let proxy_rules = map.get("proxy").unwrap_or(&HashMap::new()).clone(); + let mut proxy_rules: HashMap = HashMap::new(); + let proxy_map = map.get("proxy"); + if proxy_map.is_some() { + let proxy_map = proxy_map.unwrap(); + for k in proxy_map.keys() { + let url = proxy_map.get2(k); + if url.is_none() { + println!(); + continue; + } + let url = url.unwrap(); + proxy_rules.insert(k.clone(), url); + } + } + let map = map.get("HTEAPOT").unwrap(); Config { - port: map.get("port").unwrap_or(&"8080".to_string()).parse::().unwrap(), - host: map.get("host").unwrap_or(&"".to_string()).to_string(), - root: map.get("root").unwrap_or(&"./".to_string()).to_string(), - cache: match map.get("cache") { - Some(r) => { - if r == "true" { - true - } else { - false - } - }, - None => { - false - } - }, - index: map.get("index").unwrap_or(&"index.html".to_string()).to_string(), - error: map.get("error").unwrap_or(&"error.html".to_string()).to_string(), + port: map.get2("port").unwrap_or(8080), + host: map.get2("host").unwrap_or("".to_string()), + root: map.get2("root").unwrap_or("./".to_string()), + threads: map.get2("threads").unwrap_or(1), + cache: map.get2("cache").unwrap_or(false), + cache_ttl: map.get2("cache_ttl").unwrap_or(0), + index: map.get2("index").unwrap_or("index.html".to_string()), + error: map.get2("error").unwrap_or("error.html".to_string()), proxy_rules } } diff --git a/src/hteapot.rs b/src/hteapot.rs index 6d59e97..6892c3d 100644 --- a/src/hteapot.rs +++ b/src/hteapot.rs @@ -2,10 +2,12 @@ // This is the HTTP server module, it will handle the requests and responses // Also provide utilities to parse the requests and build the responses -use std::collections::HashMap; + + +use std::collections::{HashMap, VecDeque}; use std::hash::Hash; use std::io::{self, BufReader, BufWriter, Read, Write}; -use std::net::{TcpListener, TcpStream}; +use std::net::{Shutdown, TcpListener, TcpStream}; use std::{str, thread}; use std::sync::{Arc, Mutex, Condvar}; @@ -89,7 +91,6 @@ pub enum HttpStatus { ServiceUnavailable = 503, } - impl HttpStatus { pub fn from_u16(status: u16) -> HttpStatus { match status { @@ -145,12 +146,12 @@ pub struct HttpRequest { pub body: String, } - pub struct Hteapot { port: u16, address: String, + threads: u16, //cache: HashMap, - pool: Arc<(Mutex>, Condvar)>, + //pool: Arc<(Mutex>, Condvar)>, } @@ -161,68 +162,86 @@ impl Hteapot { Hteapot { port: port, address: address.to_string(), + threads: 1, + //cache: HashMap::new(), + + } + } + + pub fn new_threaded(address: &str, port: u16, thread: u16) -> Self { + Hteapot { + port: port, + address: address.to_string(), + threads: thread, //cache: HashMap::new(), - pool: Arc::new((Mutex::new(Vec::new()), Condvar::new())), } } + // Start the server - pub fn listen(&mut self, action: impl Fn(HttpRequest) -> Vec + Send + Sync + 'static ){ + pub fn listen(&self, action: impl Fn(HttpRequest) -> Vec + Send + Sync + 'static ){ let addr = format!("{}:{}", self.address, self.port); let listener = TcpListener::bind(addr); let listener = match listener { Ok(listener) => listener, Err(e) => { - eprintln!("Error: {}", e); + eprintln!("Error L: {}", e); return; - } + } }; + let pool: Arc<(Mutex>, Condvar)> = Arc::new((Mutex::new(VecDeque::new()), Condvar::new())); let arc_action = Arc::new(action); - listener.set_nonblocking(false).expect("set_nonblocking call failed"); - - let pool_clone = self.pool.clone(); - thread::spawn(move || { - let mut streams_to_handle = Vec::new(); - loop { - { - if streams_to_handle.is_empty() { + + + for _tn in 0..self.threads { + let pool_clone = pool.clone(); + let action_clone = arc_action.clone(); + thread::spawn(move || { + let mut streams_to_handle = Vec::new(); + loop { + { let (lock, cvar) = &*pool_clone; let mut pool = lock.lock().expect("Error locking pool"); - - while pool.is_empty(){ - pool = cvar.wait(pool).expect("Error waiting on cvar"); + if streams_to_handle.is_empty() { + pool = cvar.wait_while(pool, |pool| pool.is_empty()).expect("Error waiting on cvar"); + } - - // Movemos los streams fuera del mutex - streams_to_handle.append(&mut *pool); + + if !pool.is_empty() { + streams_to_handle.push(pool.pop_back().unwrap()); + } + } - } - streams_to_handle.retain(|stream| { - let action_clone = arc_action.clone(); - Hteapot::handle_client(stream, move |request| { - action_clone(request) - }) - }); - } - }); + + streams_to_handle.retain(|stream| { + //println!("Handling request by {}", tn); + let action_clone = action_clone.clone(); + Hteapot::handle_client(stream, move |request| { + action_clone(request) + }) + }); + } + }); + } - let pool_clone = self.pool.clone(); - for stream in listener.incoming() { - println!("new Stream"); + let pool_clone = pool.clone(); + loop { + let stream = listener.accept(); if stream.is_err() { - println!("error stream! {:?}",stream.err()); continue; } - let stream = stream.unwrap(); - println!("Getting lock"); - let (lock, cvar) = &*pool_clone; - stream.set_nodelay(true).expect("Error set nodelay to stream"); - let mut pool = lock.lock().expect("Error locking pool"); - println!("Getted!"); - - pool.push(stream); - cvar.notify_one(); // Notify one waiting thread + let (stream, _) = stream.unwrap(); + stream.set_nonblocking(true).expect("Error seting non blocking"); + stream.set_nodelay(true).expect("Error seting no delay"); + { + let (lock, cvar) = &*pool_clone; + let mut pool = lock.lock().expect("Error locking pool"); + + pool.push_front(stream); + cvar.notify_one(); + } + // Notify one waiting thread } } @@ -245,6 +264,8 @@ impl Hteapot { 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 } @@ -335,13 +356,14 @@ impl Hteapot { return true; }, _ => { + println!("{:?}",e); return false; }, } }, Ok(m) => { if m == 0 { - return false; + break; } }, }; @@ -351,25 +373,24 @@ impl Hteapot { } let request_string = String::from_utf8(request_buffer).unwrap(); + // let request_string = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n".to_string(); let request = Self::request_parser(request_string); if request.is_err() { eprintln!("{}", request.err().unwrap()); return false; } let request = request.unwrap(); - let response = action(request); let r = writer.write_all(&response); if r.is_err() { - eprintln!("Error: {}", r.err().unwrap()); + eprintln!("Error1: {}", r.err().unwrap()); } let r = writer.flush(); if r.is_err() { - eprintln!("Error: {}", r.err().unwrap()); + eprintln!("Error2: {}", r.err().unwrap()); } - - let r = reader.read(&mut [0; 1]); - r.is_err() + let _ = stream.shutdown(Shutdown::Both); + false } } @@ -391,7 +412,7 @@ fn test_http_parser() { fn test_http_response_maker() { let response = Hteapot::response_maker(HttpStatus::IAmATeapot, "Hello, World!", None); let response = String::from_utf8(response).unwrap(); - let expected_response = "HTTP/1.1 418 I'm a teapot\r\nContent-Length: 13\r\n\r\nHello, World!"; + let expected_response = "HTTP/1.1 418 I'm a teapot\r\nContent-Length: 13\r\n\r\nHello, World!\r\n"; assert_eq!(response, expected_response); } diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..e5654b8 --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,104 @@ + +use std::time::{self, SystemTime, UNIX_EPOCH}; +use std::io::{BufWriter, Write}; + +struct SimpleTime; +impl SimpleTime { + fn epoch_to_ymdhms(seconds: u64) -> (i32, u32, u32, u32, u32, u32) { + // Constants for time calculations + const SECONDS_IN_MINUTE: u64 = 60; + const SECONDS_IN_HOUR: u64 = 3600; + const SECONDS_IN_DAY: u64 = 86400; + + // Leap year and normal year days + const DAYS_IN_YEAR: [u32; 2] = [365, 366]; + const DAYS_IN_MONTH: [[u32; 12]; 2] = [ + [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], // Normal years + [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] // Leap years + ]; + + // Calculate the number of days since the epoch + let mut remaining_days = seconds / SECONDS_IN_DAY; + + // Determine the current year + let mut year = 1970; + loop { + let leap_year = if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) { 1 } else { 0 }; + if remaining_days < DAYS_IN_YEAR[leap_year] as u64 { + break; + } + remaining_days -= DAYS_IN_YEAR[leap_year] as u64; + year += 1; + } + + // Determine the current month and day + let leap_year = if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) { 1 } else { 0 }; + let mut month = 0; + while remaining_days >= DAYS_IN_MONTH[leap_year][month] as u64 { + remaining_days -= DAYS_IN_MONTH[leap_year][month] as u64; + month += 1; + } + let day = remaining_days + 1; // Days are 1-based + + // Calculate the current hour, minute, and second + let remaining_seconds = seconds % SECONDS_IN_DAY; + let hour = (remaining_seconds / SECONDS_IN_HOUR) as u32; + let minute = ((remaining_seconds % SECONDS_IN_HOUR) / SECONDS_IN_MINUTE) as u32; + let second = (remaining_seconds % SECONDS_IN_MINUTE) as u32; + + (year, month as u32 + 1, day as u32, hour, minute, second) +} + pub fn get_current_timestamp() -> String { + let now = SystemTime::now(); + let since_epoch = now.duration_since(UNIX_EPOCH).expect("Time went backwards"); + let secs = since_epoch.as_secs(); + let (year, month, day, hour, minute, second) = Self::epoch_to_ymdhms(secs); + + + format!("{:04}/{:02}/{:02} - {:02}:{:02}:{:02}", + year, month, day, hour, minute, second) + } +} + + + + +pub struct Logger { + buffers: Vec>, +} + +impl Logger { + pub fn new(writer: W) -> Logger { + let mut buffers = Vec::new(); + buffers.push(BufWriter::new(writer)); + Logger { + buffers: buffers, + } + } + + fn log(&mut self, content: String) { + for b in self.buffers.iter_mut() { + let _ = b.write(content.as_bytes()); + let _ = b.flush(); + }; + + } + pub fn add(&mut self, writer: W) { + self.buffers.push(BufWriter::new(writer)); + } + + pub fn msg(&mut self, content: String) { + self.log(format!("[{}] - {}\n",SimpleTime::get_current_timestamp() ,content)); + } + +} + +#[cfg(test)] +use std::io::stdout; + +#[test] +fn test_basic() { + + let mut logs = Logger::new(stdout()); + logs.msg("test".to_string()); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 242ef2c..c5b4605 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod logger; pub mod hteapot; mod config; mod brew; @@ -6,12 +7,17 @@ mod brew; use std::collections::HashMap; use std::fs; use std::io; +use std::io::BufWriter; +use std::net::TcpStream; use std::sync::Arc; use std::sync::Mutex; +use std::time; +use std::time::SystemTime; use hteapot::Hteapot; use hteapot::HttpStatus; use brew::fetch; +use logger::Logger; fn main() { @@ -21,11 +27,17 @@ fn main() { } else { config::Config::new_default() }; - let cache: Mutex>> = Mutex::new(HashMap::new()); - let mut server = Hteapot::new(config.host.as_str(), config.port); - println!("Server started at http://{}:{}", config.host, config.port); + let logger = Mutex::new(Logger::new(io::stdout())); + let cache: Mutex, u64)>> = Mutex::new(HashMap::new()); + 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://{}:{}", config.host, config.port)); + if config.cache { + logger.lock().expect("this doesnt work :C").msg("Cache Enabled".to_string()); + } + server.listen( move |req| { - println!("Request: {:?}", req.path); + //let mut logger = Logger::new(io::stdout()); + 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); @@ -34,7 +46,7 @@ fn main() { req.path.clone() }; if config.proxy_rules.contains_key(&req.path) { - println!("Proxying to: {}", config.proxy_rules.get(&req.path).unwrap()); + logger.lock().expect("this doesnt work :C").msg(format!("Proxying to: {}", config.proxy_rules.get(&req.path).unwrap())); let url = config.proxy_rules.get(&req.path).unwrap(); return match fetch(url) { Ok(response) => { @@ -48,32 +60,48 @@ fn main() { let path = format!("./{}/{}",config.root, path); let cache_result = { - 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 + 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 + } } + } else { + None } }; - + let mut is_cache = false; let content: Result, _> = if cache_result.is_some() { - Ok(cache_result.unwrap()) + 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) + } } else { fs::read(&path) }; match content { Ok(content) => { - { + if config.cache { let cache = cache.lock(); - if cache.is_ok() { + if cache.is_ok() && is_cache { let mut cache = cache.unwrap(); - cache.insert(path,content.clone()); + 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)); } }