diff --git a/.gitignore b/.gitignore index 2665ed5..108410b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,5 @@ target/ *.stats* .vscode -./config.json -pages/* \ No newline at end of file +config.json +pages/* diff --git a/Cargo.toml b/Cargo.toml index c7e3e4a..6e6a4ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "busser" -version = "0.0.2" +version = "0.0.3" authors = ["Jerboa"] edition="2021" diff --git a/README.md b/README.md index 12aef2b..a142feb 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,43 @@ ## Busser -Simple website hosting in Rust with Axum +#### Simple HTTPS website hosting in Rust with [Axum](https://github.com/tokio-rs/axum). -✔️ Host HTML/JS (text) content from a given directory +1. Just create a folder with your ```.html/.css/.js``` and other resources, ```.png, .gif, ...``` +2. Point Busser to it with a config.json. +3. Run it, and that's it!* + +\* you'll need certificates for https, and open ports if on a server +```json +// config.json +{ + "port_https": 443, + "port_http": 80, + "throttle": {"max_requests_per_second": 5.0, "timeout_millis": 5000, "clear_period_seconds": 86400}, + "path": "pages", + "notification_endpoint": { "addr": "https://discord.com/api/webhooks/xxx/yyy" }, + "cert_path": "certs/cert.pem", + "key_path": "certs/key.pem" +} +``` + +✔️ Host HTML content from a given directory ✔️ Http redirect to https -✔️ Https certifactes +✔️ Https certificates ✔️ IP throttling -🏗️ Host Image content (png, jpg, gif, webp, ...) +✔️ Host Image content (png, jpg, gif, webp, ...) + +✔️ Host js, css content + +✔️ Host via **free tier** cloud services! 🏗️ Discord webhook integration for status messages +____ + #### GDPR, Cookie Policies, and Privacy Policies Please be aware the following data is store/processed diff --git a/config.json b/config.json deleted file mode 100644 index df7a1ac..0000000 --- a/config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "port_https": 443, - "port_http": 80, - "throttle": {"max_requests_per_second": 5.0, "timeout_millis": 5000, "clear_period_seconds": 86400}, - "path": "pages", - "notification_endpoint": { "addr": "https://discord.com/api/webhooks/xxx/yyy" }, - "cert_path": "certs/cert.pem", - "key_path": "certs/key.pem" -} diff --git a/src/lib.rs b/src/lib.rs index c096b42..8ec5455 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,10 @@ const MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR"); const MINOR: &str = env!("CARGO_PKG_VERSION_MINOR"); const PATCH: &str = env!("CARGO_PKG_VERSION_PATCH"); +const RESOURCE_REGEX: &str = r"(\.\S+)"; +const HTML_REGEX: &str = r"(\.html)$"; +const NO_EXTENSION_REGEX: &str = r"^(?!.*\.).*"; + const DEBUG: bool = true; pub fn debug(msg: String, context: Option) diff --git a/src/pages/mod.rs b/src/pages/mod.rs index f8b6840..a513f49 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -1,8 +1,6 @@ -use std::path::Path; - use regex::Regex; -use crate::{server::model::{Config, CONFIG_PATH}, util::{list_dir, list_dir_by, list_sub_dirs, read_file_utf8}}; +use crate::{util::{list_dir_by, list_sub_dirs, read_file_utf8}, HTML_REGEX}; use self::page::Page; @@ -16,7 +14,7 @@ pub fn get_pages(path: Option<&str>) -> Vec None => "" }; - let html_regex = Regex::new(".html").unwrap(); + let html_regex = Regex::new(HTML_REGEX).unwrap(); let page_paths = list_dir_by(html_regex, scan_path.to_string()); let mut pages: Vec = vec![]; diff --git a/src/pages/page.rs b/src/pages/page.rs index 56f12cc..a50dc9f 100644 --- a/src/pages/page.rs +++ b/src/pages/page.rs @@ -1,6 +1,10 @@ +use std::cmp::min; + use axum::response::{IntoResponse, Response, Html}; use serde::{Serialize, Deserialize}; +use crate::util::read_file_utf8; + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Page { @@ -15,6 +19,15 @@ impl Page Page { uri: uri.to_string(), body: body.to_string() } } + pub fn from_file(path: String) -> Option + { + match read_file_utf8(&path) + { + Some(data) => Some(Page::new(path.as_str(), data.as_str())), + None => None + } + } + pub fn error(text: &str) -> Page { Page::new("/", text) @@ -24,6 +37,11 @@ impl Page { self.uri.clone() } + + pub fn preview(&self, n: usize) -> String + { + format!("uri: {}, body: {} ...", self.get_uri(), self.body[1..min(n, self.body.len())].to_string()) + } } impl IntoResponse for Page { diff --git a/src/resources/mod.rs b/src/resources/mod.rs index dd231cb..ddabeb2 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -1 +1,55 @@ -pub mod resource; \ No newline at end of file +pub mod resource; + +use regex::Regex; + +use crate::{util::{list_dir_by, list_sub_dirs, read_file_bytes}, HTML_REGEX, RESOURCE_REGEX}; + +use self::resource::{content_type, Resource}; + +pub fn get_resources(path: Option<&str>) -> Vec +{ + let scan_path = match path + { + Some(s) => s, + None => "" + }; + + let resource_regex = Regex::new(RESOURCE_REGEX).unwrap(); + let html_regex = Regex::new(HTML_REGEX).unwrap(); + + let resource_paths = list_dir_by(resource_regex, scan_path.to_string()); + let mut resources: Vec = vec![]; + + for resource_path in resource_paths + { + match html_regex.find_iter(resource_path.as_str()).count() + { + 0 => {}, + _ => {continue} + } + + let data = match read_file_bytes(&resource_path) + { + Some(data) => data, + None => continue + }; + + resources.push(Resource::new(resource_path.as_str(), data, content_type(resource_path.to_string()))); + } + + let dirs = list_sub_dirs(scan_path.to_string()); + + if !dirs.is_empty() + { + for dir in dirs + { + for resource in get_resources(Some(&dir)) + { + resources.push(resource); + } + } + } + + resources + +} \ No newline at end of file diff --git a/src/resources/resource.rs b/src/resources/resource.rs index e69de29..52907c0 100644 --- a/src/resources/resource.rs +++ b/src/resources/resource.rs @@ -0,0 +1,81 @@ +use std::{cmp::min, collections::HashMap}; + +use axum::response::{Html, IntoResponse, Response}; +use regex::Regex; +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Resource +{ + uri: String, + body: Vec, + content_type: String +} + +pub fn content_type(extension: String) -> &'static str +{ + let content_types = HashMap::from + ( + [ + (r"\.txt$", "text/plain"), + (r"\.css$", "text/css"), + (r"\.csv$", "text/csv"), + (r"\.(javascript|js)$", "text/javascript"), + (r"\.xml$", "text/xml"), + (r"\.gif$", "image/gif"), + (r"\.(jpg|jpeg)$", "image/jpeg"), + (r"\.png$", "image/png"), + (r"\.tiff$", "image/tiff"), + (r"\.ico$", "image/x-icon"), + (r"\.(djvu)|(djv)$", "image/vnd.djvu"), + (r"\.svg$", "image/svg+xml"), + (r"\.(mpeg|mpg|mp2|mpe|mpv|m2v)$", "video/mpeg"), + (r"\.(mp4|m4v)$", "video/mp4"), + (r"\.(qt|mov)$", "video/quicktime"), + (r"\.(wmv)$", "video/x-ms-wmv"), + (r"\.(flv|f4v|f4p|f4a|f4b)$", "video/x-flv"), + (r"\.webm$", "video/webm") + ] + ); + + for (re, content) in content_types + { + if Regex::new(re).unwrap().is_match(&extension) + { + return content + } + } + + "application/octet-stream" +} + +impl Resource +{ + pub fn new(uri: &str, body: Vec, content_type: &str) -> Resource + { + Resource { uri: uri.to_string(), body, content_type: content_type.to_string() } + } + + pub fn get_uri(&self) -> String + { + self.uri.clone() + } + + pub fn get_bytes(&self) -> Vec + { + self.body.clone() + } + + pub fn preview(&self, n: usize) -> String + { + format!("uri: {}, type: {}, bytes: {:?} ...", self.get_uri(), self.content_type, self.body[1..min(n, self.body.len())].to_vec()) + } +} + +impl IntoResponse for Resource { + fn into_response(self) -> Response { + let mut response = Html(self.body).into_response(); + response.headers_mut().insert("content-type", self.content_type.parse().unwrap()); + response + } +} \ No newline at end of file diff --git a/src/server/https.rs b/src/server/https.rs index 8aadc56..6f0380a 100644 --- a/src/server/https.rs +++ b/src/server/https.rs @@ -1,6 +1,6 @@ use crate:: { - pages::get_pages, util::read_file_utf8, web::throttle::{handle_throttle, IpThrottler} + pages::{get_pages, page::Page}, resources::get_resources, util::read_file_utf8, web::throttle::{handle_throttle, IpThrottler} }; use std::{net::{IpAddr, Ipv4Addr, SocketAddr}, path::Path}; @@ -23,6 +23,22 @@ pub struct Server config: Config } +pub fn parse_uri(uri: String, path: String) -> String +{ + if uri.starts_with(&path) + { + uri.replace(&path, "/") + } + else if uri.starts_with("/") + { + uri + } + else + { + "/".to_string()+&uri + } +} + impl Server { pub fn new @@ -77,27 +93,17 @@ impl Server let app = Arc::new(Mutex::new(AppState::new())); let pages = get_pages(Some(&config.get_path())); + let resources = get_resources(Some(&config.get_path())); let mut router: Router<(), axum::body::Body> = Router::new(); for page in pages { - crate::debug(format!("Adding page {:?}", page), None); + crate::debug(format!("Adding page {:?}", page.preview(64)), None); let path = config.get_path()+"/"; - let uri = if page.get_uri().starts_with(&path) - { - page.get_uri().replace(&path, "/") - } - else if page.get_uri().starts_with("/") - { - page.get_uri() - } - else - { - "/".to_string()+&page.get_uri() - }; + let uri = parse_uri(page.get_uri(), path); crate::debug(format!("Serving: {}", uri), None); @@ -108,6 +114,33 @@ impl Server ) } + for resource in resources + { + crate::debug(format!("Adding resource {:?}", resource.preview(8)), None); + + let path = config.get_path()+"/"; + + let uri = parse_uri(resource.get_uri(), path); + + crate::debug(format!("Serving: {}", uri), None); + + router = router.route + ( + &uri, + get(|| async move {resource.clone().into_response()}) + ) + } + + match Page::from_file(config.get_home()) + { + Some(page) => + { + crate::debug(format!("Serving home page, /, {}", page.preview(64)), None); + router = router.route("/", get(|| async move {page.clone().into_response()})) + }, + None => {} + } + router = router.layer(middleware::from_fn_with_state(throttle_state.clone(), handle_throttle)); Server diff --git a/src/server/model.rs b/src/server/model.rs index 711f650..c034ffb 100644 --- a/src/server/model.rs +++ b/src/server/model.rs @@ -36,6 +36,7 @@ pub struct Config port_https: u16, port_http: u16, path: String, + home: String, notification_endpoint: Webhook, cert_path: String, key_path: String, @@ -73,6 +74,11 @@ impl Config { self.path.clone() } + + pub fn get_home(&self) -> String + { + self.home.clone() + } pub fn get_throttle_config(&self) -> ThrottleConfig { diff --git a/src/util.rs b/src/util.rs index e2162ca..45ac124 100644 --- a/src/util.rs +++ b/src/util.rs @@ -47,11 +47,33 @@ pub fn read_file_utf8(path: &str) -> Option let mut s = String::new(); match file.read_to_string(&mut s) { + Err(why) => + { + crate::debug(format!("error reading file to utf8, {}", why), None); + None + }, + Ok(_) => Some(s) + } +} + +pub fn read_file_bytes(path: &str) -> Option> +{ + let mut file = match File::open(path) { Err(why) => { crate::debug(format!("error reading file to utf8, {}", why), None); return None }, + Ok(file) => file, + }; + + let mut s: Vec = vec![]; + match file.read_to_end(&mut s) { + Err(why) => + { + crate::debug(format!("error reading file to utf8, {}", why), None); + None + }, Ok(_) => Some(s) } } @@ -129,12 +151,8 @@ pub fn list_sub_dirs(path: String) -> Vec { match md.is_dir() { - true => found_dirs.push(p), - false => - { - crate::debug(format!("not a folder: {}", n), None); - continue - } + true => {found_dirs.push(p.clone()); crate::debug(format!("found folder: {}", p), None)}, + false => {continue} } }, Err(e) => diff --git a/tests/pages/data/b.txt b/tests/pages/data/b.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/css.css b/tests/pages/data/css.css new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/csv.csv b/tests/pages/data/csv.csv new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/gif.gif b/tests/pages/data/gif.gif new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/ico.ico b/tests/pages/data/ico.ico new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/jpg.jpg b/tests/pages/data/jpg.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/js.js b/tests/pages/data/js.js new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/mp4.gif b/tests/pages/data/mp4.gif new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/mp4.mp4 b/tests/pages/data/mp4.mp4 new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/mpeg.mpeg b/tests/pages/data/mpeg.mpeg new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/png.jpg b/tests/pages/data/png.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/png.png b/tests/pages/data/png.png new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/qt.mov b/tests/pages/data/qt.mov new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/svg.svg b/tests/pages/data/svg.svg new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/tiff.tiff b/tests/pages/data/tiff.tiff new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/vid.flv b/tests/pages/data/vid.flv new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/vid.webm b/tests/pages/data/vid.webm new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/vid.wmv b/tests/pages/data/vid.wmv new file mode 100644 index 0000000..e69de29 diff --git a/tests/pages/data/xml.xml b/tests/pages/data/xml.xml new file mode 100644 index 0000000..e69de29 diff --git a/tests/text_content_type.rs b/tests/text_content_type.rs new file mode 100644 index 0000000..1b99747 --- /dev/null +++ b/tests/text_content_type.rs @@ -0,0 +1,37 @@ +mod common; + +#[cfg(test)] +mod test_Resource_load +{ + use busser::resources::{get_resources, resource::Resource}; + + #[test] + fn test_content_types() + { + let resources = get_resources(Some("tests/pages/data")); + + assert_eq!(resources.len(), 19); + + println!("{:?}", resources); + + assert!(resources.contains(&Resource::new("tests/pages/data/b.txt", vec![], "text/plain"))); + assert!(resources.contains(&Resource::new("tests/pages/data/css.css", vec![], "text/css"))); + assert!(resources.contains(&Resource::new("tests/pages/data/csv.csv", vec![], "text/csv"))); + assert!(resources.contains(&Resource::new("tests/pages/data/gif.gif", vec![], "image/gif"))); + assert!(resources.contains(&Resource::new("tests/pages/data/ico.ico", vec![], "image/x-icon"))); + assert!(resources.contains(&Resource::new("tests/pages/data/jpg.jpg", vec![], "image/jpeg"))); + assert!(resources.contains(&Resource::new("tests/pages/data/js.js", vec![], "text/javascript"))); + assert!(resources.contains(&Resource::new("tests/pages/data/mp4.gif", vec![], "image/gif"))); + assert!(resources.contains(&Resource::new("tests/pages/data/mp4.mp4", vec![], "video/mp4"))); + assert!(resources.contains(&Resource::new("tests/pages/data/png.jpg", vec![], "image/jpeg"))); + assert!(resources.contains(&Resource::new("tests/pages/data/png.png", vec![], "image/png"))); + assert!(resources.contains(&Resource::new("tests/pages/data/qt.mov", vec![], "video/quicktime"))); + assert!(resources.contains(&Resource::new("tests/pages/data/svg.svg", vec![], "image/svg+xml"))); + assert!(resources.contains(&Resource::new("tests/pages/data/tiff.tiff", vec![], "image/tiff"))); + assert!(resources.contains(&Resource::new("tests/pages/data/vid.flv", vec![], "video/x-flv"))); + assert!(resources.contains(&Resource::new("tests/pages/data/vid.webm", vec![], "video/webm"))); + assert!(resources.contains(&Resource::new("tests/pages/data/vid.wmv", vec![], "video/x-ms-wmv"))); + assert!(resources.contains(&Resource::new("tests/pages/data/xml.xml", vec![], "text/xml"))); + } + +} \ No newline at end of file