diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index e140d19..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'afire'", - "cargo": { - "args": ["build", "--bin=afire", "--package=afire"], - "filter": { - "name": "afire", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'afire'", - "cargo": { - "args": ["test", "--no-run", "--bin=afire", "--package=afire"], - "filter": { - "name": "afire", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} diff --git a/Cargo.lock b/Cargo.lock index c1e4ee3..e97ffb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "afire" -version = "0.2.0" +version = "0.2.1" dependencies = [ "afire", ] diff --git a/Cargo.toml b/Cargo.toml index c0ffbe9..4dc2977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "afire" -version = "0.2.0" +version = "0.2.1" authors = ["Connor Slade "] edition = "2018" @@ -22,6 +22,7 @@ default = ["panic_handler", "thread_pool", "cookies", "dynamic_resize"] # Extions rate_limit = [] logging = [] +serve_static = [] # Default On panic_handler = [] @@ -31,4 +32,4 @@ dynamic_resize = [] [dev-dependencies] # Enable rate_limit and logging features for examples -afire = { path = ".", features = ["rate_limit", "logging"] } +afire = { path = ".", features = ["rate_limit", "logging", "serve_static"] } diff --git a/Changelog.md b/Changelog.md index 33a5377..323d442 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,7 +1,26 @@ +# 0.2.1 +- Only Build common::remove_address_port if logger or rate-limiter are enabled +- Make Header name / value Public +- Serve Static Middleware +- Make Routes use Closures +- Remove Threadpool (for now) +- Make Error handler use a closure +- Rename `set_error_handler` to `error_handler` +- Rename `set_socket_timeout` to `socket_timeout` +- Update Serve Static Example to use Middleware +- Allow for Manually setting the reason phrase +- Support URL encoded cookies +- Rename `add_default_header` to `default_header` +- Store Raw Request data and Request body as `Vec` +- Fix Panic Handler feature compile problems +- Dont use an Option for Vec of default headers +- Fix Header Parseing +- Add a `header` method on Request to get headers + # 0.2.0 - Response Overhaul, Now more like a Response Builder - Update *every* example with new syntax... -- Small improvement to Query parseing +- Small improvement to Query parsing - Update SetCookie Function Names - Update Cookie Example - Add a Build Script to write the Readme from the docstring in lib.rs @@ -14,12 +33,12 @@ # 0.1.7 - Add Panic Message to Error Handel -- Add http.rs to move raw http parseing out of server.rs +- Add http.rs to move raw http parsing out of server.rs - Start / Start Threaded returns Option - Add .unwrap to all server.starts in examples - Add http.rs to move raw http parsing out of server.rs - Dont give up on cookie parsing if cookie header is malformed -- Add optinal Socket Timeout +- Add optional Socket Timeout - Add Socket Timeout Docs # 0.1.6 @@ -31,7 +50,7 @@ - Ignore extra slashes in path - Remove nose.txt... don't know how that got there :P - Don't unwrap stream.read, ignore errors like a good programmer -- Fix Routeing Issue +- Fix Routing Issue - Ignore Case in Method String - Add different Reason Phrase for the status codes - Update Server Header to add Version diff --git a/README.md b/README.md index 5938a86..f30aec5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🔥 afire +# 🔥 afire A blazing fast dependency free web framework for Rust @@ -8,12 +8,12 @@ Just add the following to your `Cargo.toml`: ```toml [dependencies] -afire = "0.2.0" +afire = "0.2.1" ``` ## 📄 Info -This is kinda like express.js for rust. It is not _that_ complicated but it still makes development of apis / servers much easier. It supports Middleware and comes with some built in for Logging and Rate limiting. +This is kinda like express.js for rust. It is not _that_ complicated but it still makes development of apis / web servers much easier. It supports Middleware and comes with some built in for Static File Serving, Logging and Rate limiting. For more information on this lib check the docs [here](https://crates.io/crates/afire) @@ -44,7 +44,7 @@ server.route(Method::GET, "/", |_req| { server.start().unwrap(); // Or use multi threading *experimental* -server.start_threaded(8); +// server.start_threaded(8); ``` ## 🔧 Features @@ -59,7 +59,7 @@ For these you will need to enable the feature. To use these extra features enable them like this: ```toml -afire = { version = "0.2.0", features = ["rate_limit", "logging"] } +afire = { version = "0.2.1", features = ["rate_limit", "logging", "serve_static"] } ``` - Threading @@ -71,5 +71,5 @@ use afire::{Server, Method, Response, Header}; let mut server: Server = Server::new("localhost", 8080); -server.start_threaded(8); +// server.start_threaded(8); ``` diff --git a/examples/01_basic.rs b/examples/01_basic.rs index c2749f2..4b579ad 100644 --- a/examples/01_basic.rs +++ b/examples/01_basic.rs @@ -10,7 +10,12 @@ fn main() { // Define a handler for GET "/" server.route(Method::GET, "/", |_req| { Response::new() + // By default the status is 200 .status(200) + // By default the reason phrase is derived from the status + .reason("OK!") + // Although is is named `text` it takes any type that impls Display + // So for example numbers work too .text("Hi :P") .header(Header::new("Content-Type", "text/plain")) }); diff --git a/examples/04_data.rs b/examples/04_data.rs index 969ad8e..9b06f1a 100644 --- a/examples/04_data.rs +++ b/examples/04_data.rs @@ -31,7 +31,7 @@ fn main() { // Instead it is part of the req.body but as a string // We will need to parse it get it as a query // This is *super* easy to do with afire - let body_data = Query::from_body(req.body).unwrap(); + let body_data = Query::from_body(String::from_utf8(req.body).unwrap()).unwrap(); let name = body_data .get("name") diff --git a/examples/05_header.rs b/examples/05_header.rs index c10b3d1..8e3e0e7 100644 --- a/examples/05_header.rs +++ b/examples/05_header.rs @@ -55,7 +55,7 @@ fn main() { // If the same header is defined in the route it will be put before the default header // Although it is not garunteed to be the one picked by the client it usually is // At the bottom of this file is a representation of the order of the headers - server.add_default_header(Header::new( + server.default_header(Header::new( "X-Server-Header", "This is a server wide header", )); diff --git a/examples/06_error_handling.rs b/examples/06_error_handling.rs index 3c7214b..b0c53be 100644 --- a/examples/06_error_handling.rs +++ b/examples/06_error_handling.rs @@ -27,7 +27,7 @@ fn main() { // You can optionally define a custom error handler // This can be defined anywhere in the server and will take affect for all routes // Its like a normal route, but it will only be called if the route panics - server.set_error_handler(|_req, err| { + server.error_handler(|_req, err| { Response::new() .status(500) .text(format!( diff --git a/examples/07_serve_static.rs b/examples/07_serve_static.rs index 4a2a035..6d6f6aa 100644 --- a/examples/07_serve_static.rs +++ b/examples/07_serve_static.rs @@ -1,7 +1,7 @@ -use afire::{Header, Response, Server}; -use std::fs; +use afire::{Header, Response, ServeStatic, Server}; // Serve static files from a directory +// Afire middleware makes this *easy* const STATIC_DIR: &str = "examples/data"; @@ -9,46 +9,32 @@ fn main() { // Create a new Server instance on localhost port 8080 let mut server: Server = Server::new("localhost", 8080); - // Define a method to handle all requests - // Other methods can be defined after this one and take a higher priority - server.all(|req| { - // Gen the local path to the requested file - // Im removing '/..' in the path to avoid directory traversal exploits - let mut path = format!("{}{}", STATIC_DIR, req.path.replace("/..", "")); - - // Add Index.html if path ends with / - // This will cause the server to automatically serve the index.html - if path.ends_with('/') { - path.push_str("index.html"); - } - - // Also add '/index.html' if path dose not end with a file - // Ex 'page' will return 'page/index.html' - if !path.split('/').last().unwrap_or_default().contains('.') { - path.push_str("/index.html"); - } - - // Try to read File - // Using read over read_to_string is important to allow serving non utf8 files - match fs::read(&path) { - // If its found send it as response - // We are setting the Content-Type header with the file extension through a match expression - Ok(content) => Response::new() - .status(200) - .bytes(content) - .header(Header::new("Content-Type", get_type(&path))), - - // If not read and send 404.html - // If that file is not found, fallback to sending "Not Found :/" - Err(_) => Response::new() - .status(404) - .bytes( - fs::read(format!("{}/404.html", STATIC_DIR)) - .unwrap_or_else(|_| "Not Found :/".as_bytes().to_owned()), - ) - .header(Header::new("Content-Type", "text/html")), - } - }); + // Make a new static file server with a path + ServeStatic::new(STATIC_DIR) + // Middleware here works much diffrnetly to afire middleware + // The middleware priority is still by most recently defined + // But this middleware takes functions only - no closures + // and resultes of the middleware are put togther so more then one ac affect thre response + // + // Args: + // - req: Client Request + // - res: Current Server Response + // - suc: File to serve was found + .middleware(|req, res, suc| { + // Print path sevred + println!("Staticly Served: {}", req.path); + + // Return none to not mess with response + Some((res.header(Header::new("X-Static-Serve", "true")), suc)) + }) + // Function that runs when no file is found to serve + // This will run before middleware + .not_found(|_req, _dis| Response::new().status(404).text("Page Not Found!")) + // Add an extra mime type to the server + // It has alot already + .mime_type("key", "value") + // Attatch the middleware to the server + .attach(&mut server); println!( "[07] Listening on http://{}:{}", @@ -60,24 +46,3 @@ fn main() { // This will block the current thread server.start().unwrap(); } - -// Get the type MMIE content type of a file from its extension -// Thare are lots of other MMIME types but these are the most common -fn get_type(path: &str) -> &str { - match path.split('.').last() { - Some(ext) => match ext { - "html" => "text/html", - "css" => "text/css", - "js" => "application/javascript", - "png" => "image/png", - "jpg" => "image/jpeg", - "jpeg" => "image/jpeg", - "gif" => "image/gif", - "ico" => "image/x-icon", - "svg" => "image/svg+xml", - _ => "application/octet-stream", - }, - - None => "application/octet-stream", - } -} diff --git a/examples/10_threading.rs b/examples/10_threading.rs index 2272ba2..b0375ce 100644 --- a/examples/10_threading.rs +++ b/examples/10_threading.rs @@ -30,5 +30,5 @@ fn main() { // Start the server with 8 threads // This will block the current thread - server.start_threaded(8); + // server.start_threaded(8); } diff --git a/examples/README.md b/examples/README.md index d9a9e71..d56ec8f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -47,6 +47,8 @@ Learn about afire's automatic route error handling and add your own error handle Serve all static files from a directory. +Makes use of one of afire's built in extensions + ## 08 - Middleware Learn about Middleware and how to use it to log requests. diff --git a/examples/test.rs b/examples/test.rs new file mode 100644 index 0000000..a74df79 --- /dev/null +++ b/examples/test.rs @@ -0,0 +1,16 @@ +use afire::*; + +fn main() { + let mut server: Server = Server::new("localhost", 8081); + + server.route(Method::GET, "/", |req| { + println!("{:?}", String::from_utf8_lossy(&req.raw_data.clone())); + + Response::new() + .status(200) + .text(req.body_string().unwrap()) + .header(Header::new("Content-Type", "text/plain")) + }); + + server.start().unwrap(); +} diff --git a/lib/common.rs b/lib/common.rs index 41b834d..23ce0fd 100644 --- a/lib/common.rs +++ b/lib/common.rs @@ -64,6 +64,7 @@ pub(crate) fn reason_phrase(status: u16) -> String { /// Remove the port from an address /// /// '192.168.1.26:1234' -> '192.168.1.26' +#[cfg(any(feature = "rate_limit", feature = "logging"))] pub(crate) fn remove_address_port(address: T) -> String where T: fmt::Display, diff --git a/lib/cookie.rs b/lib/cookie.rs index 5a44052..2b8b557 100644 --- a/lib/cookie.rs +++ b/lib/cookie.rs @@ -8,7 +8,10 @@ It can be disabled with the `cookies` feature. use std::fmt; +use crate::common::decode_url; + /// Represents a Cookie +#[derive(Hash, PartialEq, Eq)] pub struct Cookie { /// Cookie Key pub name: String, @@ -78,7 +81,10 @@ impl Cookie { } .trim_end_matches(';'); - final_cookies.push(Cookie::new(name, value)); + final_cookies.push(Cookie::new( + decode_url(name.to_owned()), + decode_url(value.to_owned()), + )); } return Some(final_cookies); } diff --git a/lib/extensions/logger.rs b/lib/extensions/logger.rs index eb5c403..1848b45 100644 --- a/lib/extensions/logger.rs +++ b/lib/extensions/logger.rs @@ -171,7 +171,9 @@ impl Logger { new_path, query, headers, - req.body.replace('\n', "\\n") + String::from_utf8(req.body.clone()) + .unwrap_or_default() + .replace('\n', "\\n") )) } diff --git a/lib/extensions/mod.rs b/lib/extensions/mod.rs index 29ff2ae..5449c88 100644 --- a/lib/extensions/mod.rs +++ b/lib/extensions/mod.rs @@ -3,3 +3,6 @@ pub mod ratelimit; #[cfg(feature = "logging")] pub mod logger; + +#[cfg(feature = "serve_static")] +pub mod serve_static; diff --git a/lib/extensions/ratelimit.rs b/lib/extensions/ratelimit.rs index 4b92904..6b9dc9b 100644 --- a/lib/extensions/ratelimit.rs +++ b/lib/extensions/ratelimit.rs @@ -1,4 +1,4 @@ -use std::cell::RefCell; + use std::cell::RefCell; use std::collections::HashMap; use std::fmt; use std::time::SystemTime; diff --git a/lib/extensions/serve_static.rs b/lib/extensions/serve_static.rs new file mode 100644 index 0000000..c2f23f5 --- /dev/null +++ b/lib/extensions/serve_static.rs @@ -0,0 +1,424 @@ +use std::cell::RefCell; +use std::fs; + +use crate::Header; +use crate::Request; +use crate::Response; +use crate::Server; + +type Middleware = fn(req: Request, res: Response, success: bool) -> Option<(Response, bool)>; + +/// Serve Static Content +#[derive(Clone)] +pub struct ServeStatic { + /// Content Path + pub data_dir: String, + + /// Disabled file paths (relative from data dir) + pub disabled_files: Vec, + + /// Page not found route + pub not_found: fn(&Request, bool) -> Response, + + /// Middleware + /// + /// (Request, Static Response, Sucess [eg If file found]) + pub middleware: Vec, + + /// MIME Types + pub types: Vec<(String, String)>, +} + +impl ServeStatic { + /// Make a new Static File Server + /// ## Example + /// ```rust + /// // Import Library + /// use afire::{Server, ServeStatic}; + /// + /// // Create a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Make a new static file server and attach it to the afire server + /// ServeStatic::new("data/static").attach(&mut server); + /// + /// # server.set_run(false); + /// server.start().unwrap(); + /// ``` + pub fn new(path: T) -> Self + where + T: std::fmt::Display, + { + Self { + data_dir: path.to_string(), + disabled_files: Vec::new(), + not_found: |req, _| { + Response::new() + .status(404) + .text(format!("The page `{}` was not found...", req.path)) + .header(Header::new("Content-Type", "text/plain")) + }, + middleware: Vec::new(), + types: vec![ + ("html", "text/html"), + ("css", "text/css"), + ("js", "application/javascript"), + ("png", "image/png"), + ("jpg", "image/jpeg"), + ("jpeg", "image/jpeg"), + ("gif", "image/gif"), + ("ico", "image/x-icon"), + ("svg", "image/svg+xml"), + ("txt", "text/plain"), + ("aac", "audio/aac"), + ("avi", "video/x-msvideo"), + ("bin", "application/octet-stream"), + ("bmp", "image/bmp"), + ("bz", "application/x-bzip"), + ("bz2", "application/x-bzip2"), + ("cda", "application/x-cdf"), + ("csv", "text/csv"), + ("epub", "application/epub+zip"), + ("gz", "application/gzip"), + ("htm", "text/html"), + ("ics", "text/calendar"), + ("jar", "application/java-archive"), + ("json", "application/json"), + ("jsonld", "application/ld+json"), + ("midi", "audio/midi audio/x-midi"), + ("mid", "audio/midi audio/x-midi"), + ("mjs", "text/javascript"), + ("mp3", "audio/mpeg"), + ("mp4", "video/mp4"), + ("mpeg", "video/mpeg"), + ("oga", "audio/ogg"), + ("ogv", "video/ogg"), + ("ogx", "application/ogg"), + ("opus", "audio/opus"), + ("otf", "font/otf"), + ("pdf", "application/pdf"), + ("rar", "application/vnd.rar"), + ("rtf", "application/rtf"), + ("sh", "application/x-sh"), + ("swf", "application/x-shockwave-flash"), + ("tar", "application/x-tar"), + ("tif", "image/tiff"), + ("tiff", "image/tiff"), + ("ts", "text/x-typescript"), + ("ttf", "font/ttf"), + ("wav", "audio/wav"), + ("weba", "audio/webm"), + ("webm", "video/webm"), + ("webp", "image/webp"), + ("woff", "font/woff"), + ("woff2", "font/woff2"), + ("xhtml", "application/xhtml+xml"), + ("xml", "application/xml"), + ("zip", "application/zip"), + ("7z", "application/x-7z-compressed"), + ] + .iter() + .map(|x| (x.0.to_owned(), x.1.to_owned())) + .collect(), + } + } + + /// Disable serving a file + /// Path is relative to the dir being served + /// ## Example + /// ```rust + /// // Import Library + /// use afire::{Server, ServeStatic}; + /// + /// // Create a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Make a new static sevrer + /// ServeStatic::new("data/static") + /// // Disable a file from being served + /// .disable("index.scss") + /// // Attatch it to the afire server + /// .attach(&mut server); + /// + /// # server.set_run(false); + /// server.start().unwrap(); + /// ``` + pub fn disable(self, file_path: T) -> Self + where + T: std::fmt::Display, + { + let mut disabled = self.disabled_files; + disabled.push(file_path.to_string()); + + Self { + disabled_files: disabled, + ..self + } + } + + /// Disable serving a many files at once + /// Path is relative to the dir being served + /// ## Example + /// ```rust + /// // Import Library + /// use afire::{Server, ServeStatic}; + /// + /// // Create a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Make a new static sevrer + /// ServeStatic::new("data/static") + /// // Disable a vec of files from being served + /// .disable_vec(vec!["index.scss", "index.css.map"]) + /// // Attatch it to the afire server + /// .attach(&mut server); + /// + /// # server.set_run(false); + /// server.start().unwrap(); + /// ``` + pub fn disable_vec(self, file_paths: Vec) -> Self + where + T: std::fmt::Display, + { + let mut disabled = self.disabled_files; + for i in file_paths { + disabled.push(i.to_string()); + } + + Self { + disabled_files: disabled, + ..self + } + } + + /// Add a middleware to the static file server + /// + /// Middleware here works much diffrently to afire middleware + /// + /// The middleware priority is still by most recently defined + /// + /// But this middleware takes functions only - no closures. + /// The Resultes of the middleware are put togther so more then one middleware can affect thre response + /// ## Example + /// ```rust + /// // Import Library + /// use afire::{Server, ServeStatic}; + /// + /// // Create a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Make a new static sevrer + /// ServeStatic::new("data/static") + /// // Add some middleware to the Static File Server + /// .middleware(|req, res, suc| { + /// // Print the path of the file served + /// println!("Staticly Served: {}", req.path); + /// + /// None + /// }) + /// // Attatch it to the afire server + /// .attach(&mut server); + /// + /// # server.set_run(false); + /// server.start().unwrap(); + /// ``` + pub fn middleware(self, f: Middleware) -> Self { + let mut middleware = self.middleware; + middleware.push(f); + + Self { middleware, ..self } + } + + /// Set the not found page + /// + /// This will run if no file is found to serve or the file is disabled + /// + /// The bool in the fn parms is if the file is blocked + /// ## Example + /// ```rust + /// // Import Library + /// use afire::{Response, Server, ServeStatic}; + /// + /// // Create a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Make a new static sevrer + /// ServeStatic::new("data/static") + /// // Set a new file not found page + /// .not_found(|_req, _dis| Response::new().status(404).text("Page Not Found!")) + /// // Attatch it to the afire server + /// .attach(&mut server); + /// + /// # server.set_run(false); + /// server.start().unwrap(); + /// ``` + pub fn not_found(self, f: fn(&Request, bool) -> Response) -> Self { + Self { + not_found: f, + ..self + } + } + + /// Add a MIME type to the Static file Server + /// + /// This extension comes with alot of builtin MIME types + /// but if you need to add more thats what this is for + /// + /// The key is the file extension + /// + /// The value is the MIME type + /// ## Example + /// ```rust + /// // Import Library + /// use afire::{Server, ServeStatic}; + /// + /// // Create a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Make a new static sevrer + /// ServeStatic::new("data/static") + /// // Add a new MIME type + /// .mime_type(".3gp", "video/3gpp") + /// // Attatch it to the afire server + /// .attach(&mut server); + /// + /// # server.set_run(false); + /// server.start().unwrap(); + /// ``` + pub fn mime_type(self, key: T, value: M) -> Self + where + T: std::fmt::Display, + M: std::fmt::Display, + { + let mut types = self.types; + + types.push((key.to_string(), value.to_string())); + + Self { types, ..self } + } + + /// Add a vector of MIME type to the Static file Server + /// + /// The key is the file extension + /// + /// The value is the MIME type + /// + /// Ex: ("html", "text/html") + /// ## Example + /// ```rust + /// // Import Library + /// use afire::{Server, ServeStatic}; + /// + /// // Create a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Make a new static sevrer + /// ServeStatic::new("data/static") + /// // Add a new MIME type + /// .mime_types(vec![(".3gp", "video/3gpp")]) + /// // Attatch it to the afire server + /// .attach(&mut server); + /// + /// # server.set_run(false); + /// server.start().unwrap(); + /// ``` + pub fn mime_types(self, new_types: Vec<(T, M)>) -> Self + where + T: std::fmt::Display, + M: std::fmt::Display, + { + let mut new_types = new_types + .iter() + .map(|x| (x.0.to_string(), x.1.to_string())) + .collect(); + let mut types = self.types; + + types.append(&mut new_types); + + Self { types, ..self } + } + + /// Attatch it to a Server + /// + /// Not much to say really + /// ## Example + /// ```rust + /// // Import Library + /// use afire::{Server, ServeStatic}; + /// + /// // Create a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Make a new static sevrer + /// ServeStatic::new("data/static") + /// // Attatch it to the afire server + /// .attach(&mut server); + /// + /// # server.set_run(false); + /// server.start().unwrap(); + /// ``` + pub fn attach(self, server: &mut Server) { + let cell = RefCell::new(self); + + server.all_c(Box::new(move |req| { + let mut res = process_req(req.clone(), cell.clone()); + + for i in cell.borrow().middleware.clone().iter().rev() { + if let Some(i) = i(req.clone(), res.0.clone(), res.1) { + res = i + }; + } + + res.0 + })); + } +} + +fn process_req(req: Request, cell: RefCell) -> (Response, bool) { + let mut path = format!("{}{}", cell.borrow().data_dir, req.path.replace("/..", "")); + + // Add Index.html if path ends with / + if path.ends_with('/') { + path.push_str("index.html"); + } + + // Also add '/index.html' if path dose not end with a file + if !path.split('/').last().unwrap_or_default().contains('.') { + path.push_str("/index.html"); + } + + if cell.borrow().disabled_files.contains( + &path + .splitn(2, &cell.borrow().data_dir) + .last() + .unwrap() + .to_string(), + ) { + return ((cell.borrow().not_found)(&req, true), false); + } + + // Try to read File + match fs::read(&path) { + // If its found send it as response + Ok(content) => ( + Response::new().bytes(content).header(Header::new( + "Content-Type", + get_type(&path, &cell.borrow().types), + )), + true, + ), + + // If not send the 404 route defined + Err(_) => ((cell.borrow().not_found)(&req, false), false), + } +} + +fn get_type(path: &str, types: &[(String, String)]) -> String { + for i in types { + if i.0 == path.split('.').last().unwrap_or("") { + return i.1.to_owned(); + } + } + + "application/octet-stream".to_owned() +} diff --git a/lib/header.rs b/lib/header.rs index 74dc132..60a7b70 100644 --- a/lib/header.rs +++ b/lib/header.rs @@ -3,9 +3,13 @@ use std::fmt; /// Http header /// /// Has a name and a value. +#[derive(Hash, PartialEq, Eq)] pub struct Header { - pub(super) name: String, - pub(super) value: String, + /// Name of the Header + pub name: String, + + /// Value of the Header + pub value: String, } impl Header { @@ -65,13 +69,13 @@ impl Header { T: fmt::Display, { let header = header.to_string(); - let mut splitted_header = header.split(':'); - if splitted_header.clone().count() != 2 { + let mut split_header = header.splitn(2, ':'); + if split_header.clone().count() != 2 { return None; } Some(Header { - name: splitted_header.next()?.trim().to_string(), - value: splitted_header.next()?.trim().to_string(), + name: split_header.next()?.trim().to_string(), + value: split_header.next()?.trim().to_string(), }) } } @@ -94,12 +98,6 @@ impl fmt::Display for Header { } } -impl PartialEq for Header { - fn eq(&self, other: &Header) -> bool { - self.name == other.name && self.value == other.value - } -} - /// Stringify a Vec of headers /// /// Each header is in the format `name: value` diff --git a/lib/http.rs b/lib/http.rs index f2a1b42..b7ea9b3 100644 --- a/lib/http.rs +++ b/lib/http.rs @@ -1,15 +1,16 @@ -//! Stuff for wioking with Raw HTTP data +//! Stuff for working with Raw HTTP data #[cfg(feature = "cookies")] use super::cookie::Cookie; +use crate::common; use crate::header::Header; use crate::method::Method; use crate::query::Query; /// Get the request method of a raw HTTP request. /// -/// Defaults to GET if no methood found -pub fn get_request_method(raw_data: String) -> Method { +/// Defaults to GET if no method found +pub fn get_request_method(raw_data: &str) -> Method { let method_str = raw_data .split(' ') .next() @@ -31,7 +32,7 @@ pub fn get_request_method(raw_data: String) -> Method { } /// Get the path of a raw HTTP request. -pub fn get_request_path(raw_data: String) -> String { +pub fn get_request_path(raw_data: &str) -> String { let mut path_str = raw_data.split(' '); let path = path_str.nth(1).unwrap_or_default().to_string(); @@ -54,11 +55,12 @@ pub fn get_request_path(raw_data: String) -> String { if new_path.chars().last().unwrap_or_default() == '/' { new_path.pop(); } - new_path + + common::decode_url(new_path) } // Get The Query Data of a raw HTTP request. -pub fn get_request_query(raw_data: String) -> Query { +pub fn get_request_query(raw_data: &str) -> Query { let mut path_str = raw_data.split(' '); if path_str.clone().count() <= 1 { return Query::new_empty(); @@ -74,21 +76,26 @@ pub fn get_request_query(raw_data: String) -> Query { } /// Get the body of a raw HTTP request. -pub fn get_request_body(raw_data: String) -> String { - let mut data = raw_data.split("\r\n\r\n"); - - if data.clone().count() >= 2 { - return data - .nth(1) - .unwrap_or_default() - .trim_matches(char::from(0)) - .to_string(); +pub fn get_request_body(raw_data: &[u8]) -> Vec { + let mut raw_data = raw_data.iter().map(|x| x.to_owned()); + let mut data = Vec::new(); + for _ in raw_data.clone() { + // much jank + if raw_data.next() == Some(b'\r') + && raw_data.next() == Some(b'\n') + && raw_data.next() == Some(b'\r') + && raw_data.next() == Some(b'\n') + { + data = raw_data.collect(); + break; + } } - "".to_string() + + data } /// Get the headers of a raw HTTP request. -pub fn get_request_headers(raw_data: String) -> Vec
{ +pub fn get_request_headers(raw_data: &str) -> Vec
{ let mut headers = Vec::new(); let mut spilt = raw_data.split("\r\n\r\n"); let raw_headers = spilt.next().unwrap_or_default().split("\r\n"); @@ -104,7 +111,7 @@ pub fn get_request_headers(raw_data: String) -> Vec
{ /// Get Cookies of a raw HTTP request. #[cfg(feature = "cookies")] -pub fn get_request_cookies(raw_data: String) -> Vec { +pub fn get_request_cookies(raw_data: &str) -> Vec { let mut spilt = raw_data.split("\r\n\r\n"); let raw_headers = spilt.next().unwrap_or_default().split("\r\n"); @@ -121,7 +128,8 @@ pub fn get_request_cookies(raw_data: String) -> Vec { } /// Get the byte size of the headers of a raw HTTP request. -pub fn get_header_size(raw_data: String) -> usize { +#[cfg(feature = "dynamic_resize")] +pub fn get_header_size(raw_data: &str) -> usize { let mut headers = raw_data.split("\r\n\r\n"); headers.next().unwrap_or_default().len() + 4 } diff --git a/lib/lib.rs b/lib/lib.rs index 01ddcd6..8353646 100644 --- a/lib/lib.rs +++ b/lib/lib.rs @@ -1,5 +1,5 @@ /*! -# 🔥 afire +# 🔥 afire A blazing fast dependency free web framework for Rust @@ -9,12 +9,12 @@ Just add the following to your `Cargo.toml`: ```toml [dependencies] -afire = "0.2.0" +afire = "0.2.1" ``` ## 📄 Info -This is kinda like express.js for rust. It is not _that_ complicated but it still makes development of apis / servers much easier. It supports Middleware and comes with some built in for Logging and Rate limiting. +This is kinda like express.js for rust. It is not _that_ complicated but it still makes development of apis / web servers much easier. It supports Middleware and comes with some built in for Static File Serving, Logging and Rate limiting. For more information on this lib check the docs [here](https://crates.io/crates/afire) @@ -45,7 +45,7 @@ server.route(Method::GET, "/", |_req| { server.start().unwrap(); // Or use multi threading *experimental* -server.start_threaded(8); +// server.start_threaded(8); ``` ## 🔧 Features @@ -60,7 +60,7 @@ For these you will need to enable the feature. To use these extra features enable them like this: ```toml -afire = { version = "0.2.0", features = ["rate_limit", "logging"] } +afire = { version = "0.2.1", features = ["rate_limit", "logging", "serve_static"] } ``` - Threading @@ -73,18 +73,18 @@ use afire::{Server, Method, Response, Header}; let mut server: Server = Server::new("localhost", 8080); # server.set_run(false); -server.start_threaded(8); +// server.start_threaded(8); ``` */ #![warn(missing_docs)] -pub(crate) const VERSION: &str = "0.2.0"; +pub(crate) const VERSION: &str = "0.2.1"; mod common; mod http; -#[cfg(feature = "thread_pool")] -mod threadpool; +// #[cfg(feature = "thread_pool")] +// mod threadpool; // The main server mod server; @@ -123,9 +123,11 @@ pub use self::cookie::{Cookie, SetCookie}; // Extra Features mod extensions; -// Basic Rate Limiter #[cfg(feature = "rate_limit")] pub use extensions::ratelimit::RateLimiter; #[cfg(feature = "logging")] pub use extensions::logger::{Level, Logger}; + +#[cfg(feature = "serve_static")] +pub use extensions::serve_static::ServeStatic; diff --git a/lib/method.rs b/lib/method.rs index 1fcff0c..78a818a 100644 --- a/lib/method.rs +++ b/lib/method.rs @@ -1,6 +1,7 @@ use std::fmt; /// Methods for a request +#[derive(Hash, PartialEq, Eq)] pub enum Method { /// GET Method /// @@ -34,7 +35,7 @@ pub enum Method { /// PATCH Method /// - /// Used for aplaying a partial update to a resource + /// Used for applying a partial update to a resource PATCH, /// TRACE Method @@ -138,14 +139,3 @@ impl fmt::Debug for Method { .finish() } } - -impl PartialEq for Method { - /// Allow comparing Method Enums - /// - /// EX: Method::GET == Method::GET - /// - /// > True - fn eq(&self, other: &Self) -> bool { - std::mem::discriminant(self) == std::mem::discriminant(other) - } -} diff --git a/lib/query.rs b/lib/query.rs index 07f6ba7..65dbb76 100644 --- a/lib/query.rs +++ b/lib/query.rs @@ -2,6 +2,7 @@ use crate::common; use std::fmt; /// Struct for holding query data +#[derive(Hash, PartialEq, Eq)] pub struct Query { pub(crate) data: Vec<[String; 2]>, } diff --git a/lib/request.rs b/lib/request.rs index 52a7041..7b11ee9 100644 --- a/lib/request.rs +++ b/lib/request.rs @@ -8,6 +8,7 @@ use super::method::Method; use super::query::Query; /// Http Request +#[derive(Hash, PartialEq, Eq)] pub struct Request { /// Request method pub method: Method, @@ -26,17 +27,16 @@ pub struct Request { pub cookies: Vec, /// Request body - pub body: String, + pub body: Vec, /// Client address pub address: String, /// Raw Http Request - pub raw_data: String, + pub raw_data: Vec, } impl Request { - #[allow(clippy::too_many_arguments)] /// Quick and easy way to create a request. /// /// ```rust @@ -49,25 +49,26 @@ impl Request { /// headers: vec![], /// # #[cfg(feature = "cookies")] /// cookies: vec![], - /// body: "".to_string(), + /// body: Vec::new(), /// address: "127.0.0.1:8080".to_string(), - /// raw_data: "".to_string(), + /// raw_data: Vec::new(), /// }; /// /// # #[cfg(feature = "cookies")] - /// assert!(request.compare(&Request::new(Method::GET, "/", Query::new_empty(), vec![], vec![], "".to_string(), "127.0.0.1:8080".to_string(), "".to_string()))); + /// assert!(request.compare(&Request::new(Method::GET, "/", Query::new_empty(), vec![], vec![], Vec::new(), "127.0.0.1:8080".to_string(), Vec::new()))); /// # #[cfg(not(feature = "cookies"))] - /// # assert!(request.compare(&Request::new(Method::GET, "/", Query::new_empty(), vec![], "".to_string(), "127.0.0.1:8080".to_string(), "".to_string()))); + /// # assert!(request.compare(&Request::new(Method::GET, "/", Query::new_empty(), vec![], Vec::new(), "127.0.0.1:8080".to_string(), Vec::new()))); /// ``` + #[allow(clippy::too_many_arguments)] pub fn new( method: Method, path: &str, query: Query, headers: Vec
, #[cfg(feature = "cookies")] cookies: Vec, - body: String, + body: Vec, address: String, - raw_data: String, + raw_data: Vec, ) -> Request { Request { method, @@ -82,11 +83,41 @@ impl Request { } } - /// Compare two requests. + /// Get request body data as a string! + pub fn body_string(&self) -> Option { + String::from_utf8(self.body.clone()).ok() + } + + /// Get a request header by its name /// + /// This is not case sensitive + /// ## Example /// ```rust - /// use afire::{Request, Method}; + /// // Import Library + /// use afire::{Request, Header, Method, Query}; + /// + /// // Create Request + /// # #[cfg(feature = "cookies")] + /// let request = Request::new(Method::GET, "/", Query::new_empty(), vec![Header::new("hello", "world")], Vec::new(), Vec::new(), "127.0.0.1:8080".to_string(), Vec::new()); + /// # #[cfg(not(feature = "cookies"))] + /// # let request = Request::new(Method::GET, "/", Query::new_empty(), vec![Header::new("hello", "world")], Vec::new(), "127.0.0.1:8080".to_string(), Vec::new()); + /// + /// assert_eq!(request.header("hello").unwrap(), "world"); + /// ``` + pub fn header(&self, name: T) -> Option + where + T: fmt::Display, + { + let name = name.to_string().to_lowercase(); + for i in self.headers.clone() { + if name == i.name.to_lowercase() { + return Some(i.value); + } + } + None + } + /// Compare two requests. pub fn compare(&self, other: &Request) -> bool { self.method == other.method && self.path == other.path diff --git a/lib/response.rs b/lib/response.rs index 469cbac..587133c 100644 --- a/lib/response.rs +++ b/lib/response.rs @@ -5,6 +5,7 @@ use super::cookie::SetCookie; use super::header::Header; /// Http Response +#[derive(Hash, PartialEq, Eq)] pub struct Response { /// Response status code pub status: u16, @@ -14,12 +15,15 @@ pub struct Response { /// Response Headers pub headers: Vec
, + + /// Response Reason + pub reason: Option, } impl Response { /// Create a new Blank Response /// - /// Defult data is as follows + /// Default data is as follows /// - Status: 200 /// /// - Data: OK @@ -37,6 +41,7 @@ impl Response { status: 200, data: vec![79, 75], headers: Vec::new(), + reason: None, } } @@ -45,6 +50,7 @@ impl Response { /// ```rust /// // Import Library /// use afire::{Response, Header}; + /// /// // Create Response /// let response = Response::new() /// .status(200); // <- Here it is @@ -56,6 +62,25 @@ impl Response { } } + /// Manually set the Reason Phrase + /// ```rust + /// // Import Library + /// use afire::{Response, Header}; + /// + /// // Create Response + /// let response = Response::new() + /// .reason("OK"); + /// ``` + pub fn reason(self, reason: T) -> Response + where + T: fmt::Display, + { + Response { + reason: Some(reason.to_string()), + ..self + } + } + /// Add text as data to a Response /// /// Will accept any type that implements Display @@ -176,7 +201,7 @@ impl Response { } } -// Impl Defult for Response +// Impl Default for Response impl Default for Response { fn default() -> Response { Response::new() @@ -190,13 +215,7 @@ impl Clone for Response { status: self.status, data: self.data.clone(), headers: self.headers.clone(), + reason: self.reason.clone(), } } } - -impl PartialEq for Response { - /// Allow comparing Responses - fn eq(&self, other: &Self) -> bool { - self.status == other.status && self.data == other.data && self.headers == other.headers - } -} diff --git a/lib/route.rs b/lib/route.rs index 22dc1cb..05d9a95 100644 --- a/lib/route.rs +++ b/lib/route.rs @@ -8,35 +8,44 @@ use super::response::Response; /// /// You should not use this directly. /// It will be created automatically when using server.route -#[derive(Clone)] +// #[derive(Clone)] pub struct Route { - pub(super) method: Method, - pub(super) path: String, - pub(super) handler: fn(Request) -> Response, + /// Route Method (GET, POST, ANY, etc) + pub method: Method, + + /// Route Path + pub path: String, + + /// Route Handler + pub handler: Box Response>, } impl Route { /// Creates a new route. - pub(super) fn new(method: Method, path: String, handler: fn(Request) -> Response) -> Route { - let mut new_path = path; - if new_path.chars().last().unwrap_or_default() == '/' { - new_path.pop(); + pub(super) fn new( + method: Method, + path: String, + handler: Box Response>, + ) -> Route { + let mut path = path; + if path.chars().last().unwrap_or_default() == '/' { + path.pop(); } Route { method, - path: new_path, + path, handler, } } } +// TODO: Show handler in debug impl fmt::Debug for Route { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Route") .field("method", &self.method) .field("path", &self.path) - .field("handler", &self.handler) .finish() } } diff --git a/lib/server.rs b/lib/server.rs index 5949ab0..1397dcc 100644 --- a/lib/server.rs +++ b/lib/server.rs @@ -15,8 +15,9 @@ use std::time::Duration; #[cfg(feature = "panic_handler")] use std::panic; -#[cfg(feature = "thread_pool")] -use super::threadpool::ThreadPool; +// #[cfg(feature = "thread_pool")] +// TODO: Add Back Threadpool +// use super::threadpool::ThreadPool; // Import local files use super::common::reason_phrase; @@ -49,20 +50,20 @@ pub struct Server { /// Middleware pub middleware: Vec Option>>, - /// Run server - /// - /// Really just for testing. - run: bool, - /// Default response for internal server errors #[cfg(feature = "panic_handler")] - error_handler: fn(Request, String) -> Response, + pub error_handler: Box Response>, /// Headers automatically added to every response. - default_headers: Option>, + pub default_headers: Vec
, /// Socket Timeout - socket_timeout: Option, + pub socket_timeout: Option, + + /// Run server + /// + /// Really just for testing. + run: bool, } /// Implementations for Server @@ -97,7 +98,7 @@ impl Server { panic!("Invalid Server IP"); } for i in 0..4 { - let octet: u8 = split_ip[i].parse::().expect("Invalid Server IP"); + let octet = split_ip[i].parse::().expect("Invalid Server IP"); ip[i] = octet; } @@ -109,14 +110,14 @@ impl Server { run: true, #[cfg(feature = "panic_handler")] - error_handler: |_, err| { + error_handler: Box::new(|_, err| { Response::new() .status(500) - .text(format!("Internal Server Error :/\n{}", err)) + .text(format!("Internal Server Error :/\nError: {}", err)) .header(Header::new("Content-Type", "text/plain")) - }, + }), - default_headers: Some(vec![Header::new("Server", format!("afire/{}", VERSION))]), + default_headers: vec![Header::new("Server", format!("afire/{}", VERSION))], socket_timeout: None, } } @@ -163,12 +164,17 @@ impl Server { // Get the response from the handler // Uses the most recently defined route that matches the request - let mut res = - handle_connection(&stream, &self.middleware, self.error_handler, &self.routes); + let mut res = handle_connection( + &stream, + &self.middleware, + #[cfg(feature = "panic_handler")] + &self.error_handler, + &self.routes, + ); // Add default headers to response let mut headers = res.headers; - headers.append(&mut self.default_headers.clone().unwrap_or_default()); + headers.append(&mut self.default_headers.clone()); // Add content-length header to response headers.push(Header::new("Content-Length", &res.data.len().to_string())); @@ -178,7 +184,10 @@ impl Server { let mut response = format!( "HTTP/1.1 {} {}\r\n{}\r\n\r\n", res.status, - reason_phrase(res.status), + match res.reason { + Some(i) => i, + None => reason_phrase(res.status), + }, headers_to_string(headers) ) .as_bytes() @@ -196,89 +205,69 @@ impl Server { None } - /// Start the server with a thread pool. + /// Get the ip a server is listening on as a string /// - /// **IN DEVELOPMENT** + /// For example, "127.0.0.1" + /// ## Example + /// ```rust + /// // Import Library + /// use afire::Server; + /// + /// // Create a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Get the ip a server is listening on as a string + /// assert_eq!("127.0.0.1", server.ip_string()); + /// ``` + pub fn ip_string(&self) -> String { + let ip = self.ip; + format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]) + } + + /// Add a new default header to the response /// - /// Currently will not work with any middleware. - /// Everything else works though + /// This will be added to every response /// ## Example /// ```rust /// // Import Library - /// use afire::{Server, Response, Header, Method}; + /// use afire::{Server, Header}; /// - /// // Starts a server for localhost on port 8080 + /// // Create a server for localhost on port 8080 /// let mut server: Server = Server::new("localhost", 8080); /// - /// // Define a route - /// server.route(Method::GET, "/", |req| { - /// Response::new() - /// .status(200) - /// .text("N O S E") - /// .header(Header::new("Content-Type", "text/plain")) - /// }); + /// // Add a default header to the response + /// server.default_header(Header::new("Content-Type", "text/plain")); /// - /// // Starts the server with 8 threads - /// // This is blocking - /// // Keep the server from starting and blocking the main thread + /// // Start the server + /// // As always, this is blocking /// # server.set_run(false); - /// server.start_threaded(8); + /// server.start().unwrap(); /// ``` - #[cfg(feature = "thread_pool")] - pub fn start_threaded(&self, threads: usize) -> Option<()> { - // Exit if the server should not run - if !self.run { - return Some(()); - } - - let listener = init_listener(self.ip, self.port).unwrap(); - let pool = ThreadPool::new(threads); - - for event in listener.incoming() { - // Read stream into buffer - let stream = event.ok()?; - stream.set_read_timeout(self.socket_timeout).unwrap(); - stream.set_write_timeout(self.socket_timeout).unwrap(); - - let routes = self.routes.clone(); - let error_handler = self.error_handler; - let default_headers = self.default_headers.clone(); - - pool.execute(move || { - let mut stream = stream; - // Get the response from the handler - // Uses the most recently defined route that matches the request - let mut res = handle_connection(&stream, &Vec::new(), error_handler, &routes); - - // Add default headers to response - let mut headers = res.headers; - headers.append(&mut default_headers.unwrap_or_default()); - - // Add content-length header to response - headers.push(Header::new("Content-Length", &res.data.len().to_string())); - - // Convert the response to a string - - let mut response = format!( - "HTTP/1.1 {} {}\r\n{}\r\n\r\n", - res.status, - reason_phrase(res.status), - headers_to_string(headers) - ) - .as_bytes() - .to_vec(); - - // Add Bytes of data to response - response.append(&mut res.data); - - // Send the response - let _ = stream.write_all(&response); - stream.flush().unwrap(); - }); - } + pub fn default_header(&mut self, header: Header) { + self.default_headers.push(header); + } - // Again we should never get here - None + /// Set the socket Read / Write Timeout + /// + /// ## Example + /// ```rust + /// // Import Library + /// use std::time::Duration; + /// use afire::Server; + /// + /// // Create a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Set socket timeout + /// server.socket_timeout(Some(Duration::from_secs(1))); + /// + /// // Start the server + /// // As always, this is blocking + /// # server.set_run(false); + /// server.start().unwrap(); + /// ``` + pub fn socket_timeout(&mut self, socket_timeout: Option) { + self.socket_timeout = socket_timeout; } /// Keep a server from starting @@ -305,6 +294,37 @@ impl Server { self.run = run; } + /// Add a new middleware to the server + /// + /// Will be executed before any routes are handled + /// + /// You will have access to the request object + /// You can send a response but it will keep normal routes from being handled + /// ## Example + /// ```rust + /// // Import Library + /// use afire::{Server}; + /// + /// // Starts a server for localhost on port 8080 + /// let mut server: Server = Server::new("localhost", 8080); + /// + /// // Add some middleware + /// server.middleware(Box::new(|req| { + /// // Do something with the request + /// // Return a `None` to continue to the next middleware / route + /// // Return a `Some` to send a response + /// None + ///})); + /// + /// // Starts the server + /// // This is blocking + /// # server.set_run(false); + /// server.start().unwrap(); + /// ``` + pub fn middleware(&mut self, handler: Box Option>) { + self.middleware.push(handler); + } + /// Set the panic handler response /// /// Default response is 500 "Internal Server Error :/" @@ -321,7 +341,7 @@ impl Server { /// let mut server: Server = Server::new("localhost", 8080); /// /// // Set the panic handler response - /// server.set_error_handler(|_req, err| { + /// server.error_handler(|_req, err| { /// Response::new() /// .status(500) /// .text(format!("Internal Server Error: {}", err)) @@ -332,76 +352,81 @@ impl Server { /// server.start().unwrap(); /// ``` #[cfg(feature = "panic_handler")] - pub fn set_error_handler(&mut self, res: fn(Request, String) -> Response) { - self.error_handler = res; + pub fn error_handler(&mut self, res: fn(Request, String) -> Response) { + self.error_handler = Box::new(res); } - /// Get the ip a server is listening on as a string + /// Define the panic handler but with closures! /// - /// For example, "127.0.0.1" - /// ## Example - /// ```rust - /// // Import Library - /// use afire::Server; + /// Basicity just [`Server::error_handler`] but with closures /// - /// // Create a server for localhost on port 8080 - /// let mut server: Server = Server::new("localhost", 8080); + /// Default response is 500 "Internal Server Error :/" /// - /// // Get the ip a server is listening on as a string - /// assert_eq!("127.0.0.1", server.ip_string()); - /// ``` - pub fn ip_string(&self) -> String { - let ip = self.ip; - format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]) - } - - /// Add a new default header to the response + /// This is only available if the `panic_handler` feature is enabled /// - /// This will be added to every response + /// Make sure that this wont panic because then the thread will crash /// ## Example /// ```rust /// // Import Library - /// use afire::{Server, Header}; + /// use afire::{Server, Response}; /// /// // Create a server for localhost on port 8080 /// let mut server: Server = Server::new("localhost", 8080); /// - /// // Add a default header to the response - /// server.add_default_header(Header::new("Content-Type", "text/plain")); + /// // Set the panic handler response + /// server.error_handler_c(Box::new(|_req, err| { + /// Response::new() + /// .status(500) + /// .text(format!("Internal Server Error: {}", err)) + /// })); /// /// // Start the server - /// // As always, this is blocking /// # server.set_run(false); /// server.start().unwrap(); /// ``` - pub fn add_default_header(&mut self, header: Header) { - self.default_headers - .as_mut() - .unwrap_or(&mut Vec::
::new()) - .push(header); + #[cfg(feature = "panic_handler")] + pub fn error_handler_c(&mut self, res: Box Response>) { + self.error_handler = res; } - /// Set the socket Read / Write Timeout + /// Create a new route the runs for all methods and paths /// + /// May be useful for a 404 page as the most recently defined route takes priority + /// so by defining this route first it would trigger if nothing else matches /// ## Example /// ```rust /// // Import Library - /// use std::time::Duration; - /// use afire::Server; + /// use afire::{Server, Response, Header, Method}; /// - /// // Create a server for localhost on port 8080 + /// // Starts a server for localhost on port 8080 /// let mut server: Server = Server::new("localhost", 8080); /// - /// // Set socket timeout - /// server.set_socket_timeout(Some(Duration::from_secs(1))); + /// // Define 404 page + /// // Because this is defined first, it will take a low priority + /// server.all(|req| { + /// Response::new() + /// .status(404) + /// .text("The page you are looking for does not exist :/") + /// .header(Header::new("Content-Type", "text/plain")) + /// }); /// - /// // Start the server - /// // As always, this is blocking + /// // Define a route + /// // As this is defined last, it will take a high priority + /// server.route(Method::GET, "/nose", |req| { + /// Response::new() + /// .status(200) + /// .text("N O S E") + /// .header(Header::new("Content-Type", "text/plain")) + /// }); + /// + /// // Starts the server + /// // This is blocking /// # server.set_run(false); /// server.start().unwrap(); /// ``` - pub fn set_socket_timeout(&mut self, socket_timeout: Option) { - self.socket_timeout = socket_timeout; + pub fn all(&mut self, handler: fn(Request) -> Response) { + self.routes + .push(Route::new(Method::ANY, "*".to_string(), Box::new(handler))); } /// Create a new route the runs for all methods and paths @@ -418,12 +443,12 @@ impl Server { /// /// // Define 404 page /// // Because this is defined first, it will take a low priority - /// server.all(|req| { + /// server.all_c(Box::new(|req| { /// Response::new() /// .status(404) /// .text("The page you are looking for does not exist :/") /// .header(Header::new("Content-Type", "text/plain")) - /// }); + /// })); /// /// // Define a route /// // As this is defined last, it will take a high priority @@ -439,12 +464,14 @@ impl Server { /// # server.set_run(false); /// server.start().unwrap(); /// ``` - pub fn all(&mut self, handler: fn(Request) -> Response) { + pub fn all_c(&mut self, handler: Box Response>) { self.routes .push(Route::new(Method::ANY, "*".to_string(), handler)); } /// Create a new route for any type of request + /// + /// Basicity just [`Server::any`] but with closures /// ## Example /// ```rust /// // Import Library @@ -473,63 +500,70 @@ impl Server { T: fmt::Display, { self.routes - .push(Route::new(Method::ANY, path.to_string(), handler)); + .push(Route::new(Method::ANY, path.to_string(), Box::new(handler))); } - /// Add a new middleware to the server - /// - /// Will be executed before any routes are handled - /// - /// You will have access to the request object - /// You can send a response but it will keep normal routes from being handled + /// Create a new route for specified requests /// ## Example /// ```rust /// // Import Library - /// use afire::{Server}; + /// use afire::{Server, Response, Header, Method}; /// - /// // Starts a server for localhost on port 8080 + /// // Create a server for localhost on port 8080 /// let mut server: Server = Server::new("localhost", 8080); /// - /// // Add some middleware - /// server.middleware(Box::new(|req| { - /// // Do something with the request - /// // Return a `None` to continue to the next middleware / route - /// // Return a `Some` to send a response - /// None - ///})); + /// // Define a route + /// server.route(Method::GET, "/nose", |req| { + /// Response::new() + /// .status(200) + /// .text("N O S E") + /// .header(Header::new("Content-Type", "text/plain")) + /// }); /// /// // Starts the server /// // This is blocking /// # server.set_run(false); /// server.start().unwrap(); /// ``` - pub fn middleware(&mut self, handler: Box Option>) { - self.middleware.push(handler); + pub fn route(&mut self, method: Method, path: T, handler: fn(Request) -> Response) + where + T: fmt::Display, + { + self.routes + .push(Route::new(method, path.to_string(), Box::new(handler))); } - /// Create a new route for specified requests + /// Define a new route with a closure as a handler + /// + /// Basicity just [`Server::route`] but with closures /// ## Example /// ```rust /// // Import Library /// use afire::{Server, Response, Header, Method}; /// + /// use std::cell::RefCell; + /// /// // Create a server for localhost on port 8080 /// let mut server: Server = Server::new("localhost", 8080); /// - /// // Define a route - /// server.route(Method::GET, "/nose", |req| { + /// let cell = RefCell::new(0); + /// + /// // Define a route with a closure + /// server.route_c(Method::GET, "/nose", Box::new(move |req| { + /// cell.replace_with(|&mut old| old + 1); + /// /// Response::new() /// .status(200) /// .text("N O S E") /// .header(Header::new("Content-Type", "text/plain")) - /// }); + /// })); /// /// // Starts the server /// // This is blocking /// # server.set_run(false); /// server.start().unwrap(); /// ``` - pub fn route(&mut self, method: Method, path: T, handler: fn(Request) -> Response) + pub fn route_c(&mut self, method: Method, path: T, handler: Box Response>) where T: fmt::Display, { @@ -542,7 +576,7 @@ impl Server { fn handle_connection( mut stream: &TcpStream, middleware: &[Box Option>], - error_handler: fn(Request, String) -> Response, + #[cfg(feature = "panic_handler")] error_handler: &dyn Fn(Request, String) -> Response, routes: &[Route], ) -> Response { // Init (first) Buffer @@ -554,9 +588,14 @@ fn handle_connection( Err(_) => return quick_err("Error Reading Stream", 500), }; - // Get buffer as string - let buffer_clone = buffer.clone(); - let stream_string = match str::from_utf8(&buffer_clone) { + println!( + "DUMP: {}", + String::from_utf8(buffer.clone()).unwrap_or_default() + ); + + // Get Buffer as string for parseing content length header + #[cfg(feature = "dynamic_resize")] + let stream_string = match str::from_utf8(&buffer) { Ok(s) => s, Err(_) => return quick_err("Currently no support for non utf-8 characters...", 500), }; @@ -564,13 +603,13 @@ fn handle_connection( // Get Content-Length header // If header shows thar more space is needed, // make a new buffer read the rest of the stream and add it to the first buffer - // This could cause a proformance hit but is actually seams to be fasy enough + // This could cause a performance hit but is actually seams to be fast enough #[cfg(feature = "dynamic_resize")] - for i in http::get_request_headers(stream_string.to_string()) { + for i in http::get_request_headers(stream_string) { if i.name != "Content-Length" { continue; } - let header_size = http::get_header_size(stream_string.to_string()); + let header_size = http::get_header_size(stream_string); let content_length = i.value.parse::().unwrap_or(0); let new_buffer_size = content_length as i64 + header_size as i64 - BUFF_SIZE as i64; if new_buffer_size > 0 { @@ -584,20 +623,24 @@ fn handle_connection( break; } - // TODO: Make this work with non utf8 stuff too + while buffer.ends_with(&[b'\0']) { + buffer.pop(); + } + + // Get Buffer as string for parseing Path, Method, Query, etc let stream_string = match str::from_utf8(&buffer) { - Ok(i) => i, - Err(_) => return quick_err("No support for non utf-8 chars\nFor now", 500), + Ok(s) => s, + Err(_) => return quick_err("Currently no support for non utf-8 characters...", 500), }; // Make Request Object - let req_method = http::get_request_method(stream_string.to_string()); - let req_path = http::get_request_path(stream_string.to_string()); - let req_query = http::get_request_query(stream_string.to_string()); - let body = http::get_request_body(stream_string.to_string()); - let headers = http::get_request_headers(stream_string.to_string()); + let req_method = http::get_request_method(stream_string); + let req_path = http::get_request_path(stream_string); + let req_query = http::get_request_query(stream_string); + let body = http::get_request_body(&buffer); + let headers = http::get_request_headers(stream_string); #[cfg(feature = "cookies")] - let cookies = http::get_request_cookies(stream_string.to_string()); + let cookies = http::get_request_cookies(stream_string); let req = Request::new( req_method, &req_path, @@ -607,7 +650,7 @@ fn handle_connection( cookies, body, stream.peer_addr().unwrap().to_string(), - stream_string.to_string(), + buffer, ); // Use middleware to handle request @@ -627,7 +670,9 @@ fn handle_connection( // Optionally enable automatic panic handling #[cfg(feature = "panic_handler")] { - let result = panic::catch_unwind(|| (route.handler)(req.clone())); + // let handler = .clone(); + let result = + panic::catch_unwind(panic::AssertUnwindSafe(|| (route.handler)(req.clone()))); let err = match result { Ok(i) => return i, Err(e) => match e.downcast_ref::<&str>() { @@ -652,7 +697,7 @@ fn handle_connection( .header(Header::new("Content-Type", "text/plain")) } -/// Init Listaner +/// Init Listener fn init_listener(ip: [u8; 4], port: u16) -> Result { TcpListener::bind(SocketAddr::new( IpAddr::V4(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3])), diff --git a/testing/src/main.rs b/testing/src/main.rs deleted file mode 100644 index 3e47d74..0000000 --- a/testing/src/main.rs +++ /dev/null @@ -1,73 +0,0 @@ -use afire::*; -use std::fs; - -fn main() { - let mut server: Server = Server::new("0.0.0.0", 1234); - - // Define a catch-all handler - // This will be called when no other handlers match - server.all(|_req| { - Response::new( - 404, - "Not Found", - vec![Header::new("Content-Type", "text/plain")], - ) - }); - - // Define a handler for route "/" - server.route(Method::GET, "/", |_req| { - Response::new( - 200, - "Hi :P", - vec![Header::new("Content-Type", "text/plain")], - ) - }); - - // Define a handler for route "/nose" - server.route(Method::GET, "/nose", |_req| { - Response::new( - 200, - "N O S E", - vec![Header::new("Content-Type", "text/plain")], - ) - }); - - // Define a handler for ANY "/hi" - server.any("/hi", |_req| { - Response::new( - 200, - "

Hello, How are you?

", - vec![Header::new("Content-Type", "text/html")], - ) - }); - - // Serve a file - server.route(Method::GET, "/pi", |_req| { - Response::new( - 200, - // Html stored as txt because yes - &fs::read_to_string("data/index.txt").unwrap(), - vec![Header::new("Content-Type", "text/html")], - ) - }); - - server.route(Method::GET, "/connorcode", |_req| { - Response::new( - 301, - "Hello, Connor", - vec![ - Header::new("Content-Type", "text/plain"), - Header::new("Location", "http://connorcode.com"), - ], - ) - }); - - server.every(|req| { - println!("req: {:?}", req); - None - }); - - // Start the server - println!("[*] Serving on {}:{}", server.ip_string(), server.port); - server.start(); -}