diff --git a/Cargo.lock b/Cargo.lock index 97a6ada..3c641ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -492,6 +492,7 @@ dependencies = [ "governor", "idna 0.3.0", "lazy_static", + "lib-humus", "maxminddb", "mime", "parking_lot", @@ -1011,6 +1012,19 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lib-humus" +version = "0.1.0" +source = "git+https://codeberg.org/slatian/lib-humus.git#5c9315b314ba0c247d236d6318041c6872c90b04" +dependencies = [ + "axum", + "axum-extra", + "mime", + "serde", + "tera", + "toml", +] + [[package]] name = "libc" version = "0.2.149" diff --git a/Cargo.toml b/Cargo.toml index 6ce57bf..4ddeba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ authors = ["Slatian "] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +lib-humus = { version="0.1", features=["axum-view+cookie"], git="https://codeberg.org/slatian/lib-humus.git" } + axum = { version = "0.6", features = ["macros", "headers"] } axum-extra = { version = "0.7", features = ["cookie"] } axum-client-ip = "0.4" diff --git a/src/main.rs b/src/main.rs index efad829..3eabf73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,11 +34,14 @@ use std::collections::HashMap; use std::net::IpAddr; use std::sync::Arc; +use lib_humus::TemplateEngineLoader; +use lib_humus::read_toml_from_file; +use lib_humus::HumusEngine; + mod config; mod geoip; mod idna; mod ipinfo; -mod mycelium; mod ratelimit; mod settings; mod simple_dns; @@ -51,15 +54,12 @@ use crate::geoip::{ LocationResult, }; use crate::idna::IdnaName; -use crate::mycelium::MycEngine; -use crate::mycelium::TemplateEngineLoader; -use crate::mycelium::read_toml_from_file; use crate::simple_dns::DnsLookupResult; use crate::settings::*; use crate::view::View; use crate::ipinfo::{AddressCast,AddressInfo,AddressScope}; -type TemplatingEngine = MycEngine; +type TemplatingEngine = HumusEngine; #[derive(Deserialize, Serialize, Clone)] pub struct SettingsQuery { diff --git a/src/mycelium/engine.rs b/src/mycelium/engine.rs deleted file mode 100644 index 9c60867..0000000 --- a/src/mycelium/engine.rs +++ /dev/null @@ -1,146 +0,0 @@ - -use axum::{ - body::{Bytes,Full}, - headers::HeaderValue, - http::StatusCode, - http::header, - http::header::SET_COOKIE, - response::IntoResponse, - response::Response, -}; -use tera::Tera; -use toml::Table; - -use std::marker::PhantomData; - -use crate::mycelium::MycView; -use crate::mycelium::MycFormat; -use crate::mycelium::MycFormatFamily; -use crate::mycelium::MycQuerySettings; - -/* The engine itself */ - -/// A constructable version of MycEngine without the types attached. -/// -/// Can be converted to a MycEngine using `into()`. -pub struct MycProtoEngine { - pub tera: Tera, - pub template_config: Option, -} - -#[derive(Clone)] -pub struct MycEngine -where V: MycView, S: MycQuerySettings, F: MycFormat { - pub tera: Tera, - pub template_config: Option
, - - phantom_view: PhantomData, - phantom_settings: PhantomData, - phantom_format: PhantomData, -} - -impl MycEngine -where V: MycView, S: MycQuerySettings, F: MycFormat { - - pub fn new(tera: Tera, template_config: Option
) -> Self { - Self { - tera: tera, - template_config: template_config, - phantom_view: PhantomData, - phantom_settings: PhantomData, - phantom_format: PhantomData, - } - } - - /// Takes settings and a view, converting it to a serveable response. - pub async fn render_view( - &self, - settings: &S, - view: V, - ) -> Response { - let status_code = view.get_status_code(&settings); - let cookie_string = view.get_cookie_header(&settings); - let format = settings.get_format(); - - let mut response = match format.get_family() { - MycFormatFamily::Template => { - let template_name = view.get_template_name(); - let mime_type = format.get_mime_type(); - - let mut context = tera::Context::new(); - context.insert("view", &template_name); - //intented for shared macros - context.insert("format", &format.get_name()); - context.insert("mimetype", &mime_type.to_string()); - context.insert("data", &view); - context.insert("extra", &self.template_config); - settings.initalize_template_context(&mut context); - - match self.tera.render(&(template_name.clone()+&format.get_file_extension()), &context) { - Ok(text) => { - let mut response = ( - [( - header::CONTENT_TYPE, - HeaderValue::from_str(mime_type.as_ref()) - .expect("MimeType should always be a valid header value.") - )], - Into::>::into(text), - ).into_response(); - view.update_response(&mut response, &settings); - response - }, - Err(e) => { - println!("There was an error while rendering template {}: {e:?}", template_name); - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Template error in {}, contact owner or see logs.\n", template_name) - ).into_response() - } - } - } - MycFormatFamily::API => { - view.get_api_response(&settings) - } - }; - - // Everything went well and nobody did the following work for us. - if response.status() == StatusCode::OK { - - // Set status code - if status_code != StatusCode::OK { - *response.status_mut() = status_code; - } - - // Set cookie header - if let Some(cookie_string) = cookie_string { - if let Ok(header_value) = HeaderValue::from_str(&cookie_string) { - response.headers_mut().append( - SET_COOKIE, - header_value, - ); - } - } - - } - - // return response - response - } -} - -impl From for MycEngine -where V: MycView, S: MycQuerySettings, F: MycFormat { - fn from(e: MycProtoEngine) -> Self { - Self::new(e.tera, e.template_config) - } -} - -impl From> for MycProtoEngine -where V: MycView, S: MycQuerySettings, F: MycFormat { - fn from(e: MycEngine) -> Self { - Self { - tera: e.tera, - template_config: e.template_config, - } - } -} diff --git a/src/mycelium/format.rs b/src/mycelium/format.rs deleted file mode 100644 index 274a095..0000000 --- a/src/mycelium/format.rs +++ /dev/null @@ -1,132 +0,0 @@ - -use serde::Deserialize; -use serde::Serialize; -use mime::Mime; - -/// Defines how the response should be rendered. -pub enum MycFormatFamily { - /// When rendering the templating engine will be invoked - Template, - /// When rendering the [View](./trait.View.html) - /// is asked to generate an API response. - API, -} - -/// Implement on a type that is able to describe a response format. -/// -/// It is best implemented on an enum. -pub trait MycFormat: ToString+Clone+Default+Send { - - // Return the format family this - fn get_family(&self) -> MycFormatFamily { - match self.get_name().as_str() { - "json" => MycFormatFamily::API, - _ => MycFormatFamily::Template, - } - } - - /// Returns the file extnsion for the format. - /// - /// Used for deriving the path for the template name. - /// - /// Defaults to `.{self.get_name()}` - /// with the exception of the name being `text` - /// then it defaults to `.txt`. - fn get_file_extension(&self) -> String { - match self.get_name().as_str() { - "text" => ".txt".to_string(), - _ => ".".to_owned()+&self.get_name(), - } - } - - /// Returns the name of the format, - /// by default taken from the ToString implementation. - fn get_name(&self) -> String { - self.to_string() - } - - /// Allows adding extra mimetypes quickly for prototyping - /// - /// Implementing get_mime_type() properly is recommended - /// for production use. - fn get_less_well_known_mimetype(&self) -> Option { - None - } - - /// Returns a textual representation of the Mimetype. - /// - /// It is recommended to implement this when in production use. - /// - /// For prototyping the default implementation makes assumptions - /// based on the output of get_name(), falling back - /// to get_less_well_known_mimetype() and the "application/octet-stream" type. - /// - /// The default implementation knows the following associations: - /// * `text`: `text/plain; charset=utf-8` - /// * `html`: `text/html; charset=utf-8` - /// * `json`: `application/json` - /// * `xml`: `application/xml` - /// * `rss`: `application/rss+xml` - /// * `atom`: `application/atom+xml` - /// - /// *Implementation Note:* It may be possible that two different views - /// have the same MimeType (maybe two json representations for different consumers). - /// - fn get_mime_type(&self) -> Mime { - match self.get_name().as_str() { - "text" => mime::TEXT_PLAIN_UTF_8, - "html" => mime::TEXT_HTML_UTF_8, - "json" => mime::APPLICATION_JSON, - "xml" => "application/xml".parse() - .expect("Parse static application/xml"), - "rss" => "application/rss+xml".parse() - .expect("Parse static application/rss+xml"), - "atom" => "application/atom+xml".parse() - .expect("Parse static application/atom+xml"), - _ => - self.get_less_well_known_mimetype() - .unwrap_or(mime::APPLICATION_OCTET_STREAM), - } - } - - /// Constructs a view from its name. - fn from_name(name: &str) -> Option; -} - -// Some Sample implementations - -#[derive(Clone,Serialize,Deserialize,Default)] -#[serde(rename_all="lowercase")] -pub enum HtmlTextJsonFormat { - #[default] - Html, - Text, - Json, -} - -impl ToString for HtmlTextJsonFormat { - fn to_string(&self) -> String { - match self { - Self::Html => "html", - Self::Text => "text", - Self::Json => "json", - }.to_owned() - } -} - - -impl MycFormat for HtmlTextJsonFormat { - - //TODO: implement other methods to make it more performant - - fn from_name(name: &str) -> Option { - match name { - "html" => Some(Self::Html), - "text" => Some(Self::Text), - "json" => Some(Self::Json), - _ => None, - } - } -} - - diff --git a/src/mycelium/mod.rs b/src/mycelium/mod.rs deleted file mode 100644 index a193184..0000000 --- a/src/mycelium/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod engine; -mod format; -mod query_settings; -mod template_loader; -mod toml_helper; -mod view; - -pub use self::engine::MycEngine; -pub use self::engine::MycProtoEngine; -pub use self::format::HtmlTextJsonFormat; -pub use self::format::MycFormat; -pub use self::format::MycFormatFamily; -pub use self::query_settings::MycQuerySettings; -pub use self::template_loader::TemplateEngineLoader; -pub use self::toml_helper::read_toml_from_file; -pub use self::toml_helper::TomlError; -pub use self::view::MycView; diff --git a/src/mycelium/query_settings.rs b/src/mycelium/query_settings.rs deleted file mode 100644 index 3b90937..0000000 --- a/src/mycelium/query_settings.rs +++ /dev/null @@ -1,16 +0,0 @@ - -use tera::Context; - -use crate::mycelium::MycFormat; - -pub trait MycQuerySettings -where F: MycFormat { - - /// Called before rendering a template to initalize it with - /// values that come from the query itself. - fn initalize_template_context(&self, context: &mut Context); - - /// Returns the requested output format - fn get_format(&self) -> F; - -} diff --git a/src/mycelium/template_loader.rs b/src/mycelium/template_loader.rs deleted file mode 100644 index 9726081..0000000 --- a/src/mycelium/template_loader.rs +++ /dev/null @@ -1,114 +0,0 @@ - -use tera::Tera; -use serde::Deserialize; - -use std::fmt; - -use crate::mycelium::MycProtoEngine; -use crate::mycelium::read_toml_from_file; -use crate::mycelium::TomlError; - -#[derive(Deserialize, Clone)] -pub struct TemplateEngineLoader { - pub template_location: String, - pub extra_config_location: Option, -} - -impl TemplateEngineLoader { - - pub fn new( - template_location: String, - extra_config_location: Option - ) -> Self { - Self { - template_location: template_location, - extra_config_location: extra_config_location, - } - } - - pub fn cli_template_location(mut self, location: Option) -> Self { - if let Some(location) = location { - self.template_location = location; - } - self - } - - pub fn cli_extra_config_location(mut self, location: Option) -> Self { - if let Some(location) = location { - self.extra_config_location = Some(location); - } - self - } - - // Returns the template base directory. - // - // Contructed by ensuring the template_location ends in a "/". - pub fn base_dir(&self) -> String { - if self.template_location.ends_with("/") { - self.template_location.clone() - } else { - self.template_location.clone()+"/" - } - } - - pub fn load_templates( - &self - ) -> Result { - let template_base_dir = self.base_dir(); - let template_extra_config_res = match &self.extra_config_location { - Some(path) => read_toml_from_file(path), - None => { - read_toml_from_file(&(template_base_dir.clone()+"extra.toml")) - } - }; - let template_extra_config = match template_extra_config_res { - Ok(c) => Some(c), - Err(e) => match &e { - TomlError::FileError{..} => { - // Only fatal if the file was explicitly requested. - // An implicit request could also mean that - // the template doesn't need a config file. - if self.extra_config_location.is_some() { - return Err(TemplateEngineLoaderError::TomlError(e)); - } - None - }, - TomlError::ParseError{..} => { - return Err(TemplateEngineLoaderError::TomlError(e)); - } - }, - }; - let template_glob = template_base_dir.clone()+"*"; - println!("Parsing Templates from '{}' ...", &template_glob); - let res = Tera::new((template_glob).as_str()); - let tera = match res { - Ok(t) => t, - Err(e) => { - return Err(TemplateEngineLoaderError::TemplateParseError{ - path: template_glob, - tera_error: e, - }); - } - }; - Ok(MycProtoEngine { - tera: tera, - template_config: template_extra_config, - }) - } -} - -pub enum TemplateEngineLoaderError { - TomlError(TomlError), - TemplateParseError{path: String, tera_error: tera::Error }, -} - -impl fmt::Display for TemplateEngineLoaderError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::TomlError(e) => - write!(f,"Error with template extra configuration:\n(Depending on you template the file not being found might be okay)\n{e}"), - Self::TemplateParseError{path, tera_error} => - write!(f,"Error parsing template '{path}':\n{tera_error}"), - } - } -} diff --git a/src/mycelium/toml_helper.rs b/src/mycelium/toml_helper.rs deleted file mode 100644 index ac9f0b0..0000000 --- a/src/mycelium/toml_helper.rs +++ /dev/null @@ -1,42 +0,0 @@ -use serde::Deserialize; - -use std::fmt; -use std::fs; - -pub fn read_toml_from_file(path: &String) -> Result -where T: for<'de> Deserialize<'de> { - let text = match fs::read_to_string(path) { - Ok(t) => t, - Err(e) => { - return Err(TomlError::FileError{ - path: path.clone(), - io_error: e - }); - } - }; - match toml::from_str(&text) { - Ok(t) => Ok(t), - Err(e) => { - return Err(TomlError::ParseError{ - path: path.clone(), - toml_error: e - }); - } - } -} - -pub enum TomlError { - FileError{ path: String, io_error: std::io::Error }, - ParseError{ path: String, toml_error: toml::de::Error }, -} - -impl fmt::Display for TomlError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::FileError{path, io_error} => - write!(f,"Error while reading file '{path}': {io_error}"), - Self::ParseError{path, toml_error} => - write!(f,"Unable to parse file as toml '{path}':\n{toml_error}"), - } - } -} diff --git a/src/mycelium/view.rs b/src/mycelium/view.rs deleted file mode 100644 index 1550a05..0000000 --- a/src/mycelium/view.rs +++ /dev/null @@ -1,51 +0,0 @@ -use axum::response::IntoResponse; -use axum::response::Json; -use axum::response::Response; -use axum::http::status::StatusCode; -use serde::Serialize; - -use crate::mycelium::MycQuerySettings; -use crate::mycelium::MycFormat; - -pub trait MycView: Serialize + Sized -where S: MycQuerySettings, F: MycFormat { - - /// Returns the template name that will be used to select - /// the template file. - /// - /// If the name is "404" for an html response the template - /// file "404.html" will be used. - /// - /// Also ends up as the `view` variable in the template. - fn get_template_name(&self) -> String; - - //TODO: Add query settings to these - - /// Returns the reponse code for the view. - /// - /// The numeric value will be useable as `http_status` in the template. - fn get_status_code(&self, settings: &S) -> StatusCode; - - /// If this returns a String it will be used as the cookie header. - /// - /// See: [axum: Constructing a Cookie](https://docs.rs/axum-extra/0.8.0/axum_extra/extract/cookie/struct.Cookie.html#constructing-a-cookie) - fn get_cookie_header(&self, _settings: &S) -> Option { None } - - /// Update non-API responses after they have been built. - /// - /// Useful for setting extra headers. Does noting by default. - fn update_response(&self, _response: &mut Response, _settings: &S) {} - - /// Return an API-Response - /// - /// By default causes the view to Serialize itself - /// to a json response using serde. - /// - /// Response code and cookie headers are queried in - /// advance and set on the reulting response if it has a status code of 200. - /// Otherwise it is assumed that the response genrating logic - /// alredy took care of that. - fn get_api_response(self, _settings: &S) -> Response { - Json(self).into_response() - } -} diff --git a/src/settings.rs b/src/settings.rs index cb13531..fd4be02 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,8 +1,9 @@ use serde::{Deserialize,Serialize}; +use lib_humus::HtmlTextJsonFormat; +use lib_humus::HumusQuerySettings; + use std::sync::Arc; -use crate::mycelium::HtmlTextJsonFormat; -use crate::mycelium::MycQuerySettings; /* Response format */ @@ -25,7 +26,7 @@ pub struct Selectable { pub weight: i32, } -impl MycQuerySettings for QuerySettings { +impl HumusQuerySettings for QuerySettings { fn initalize_template_context(&self, context: &mut tera::Context) { context.insert("language", &self.lang); diff --git a/src/view.rs b/src/view.rs index 5346ed3..148fe4f 100644 --- a/src/view.rs +++ b/src/view.rs @@ -5,6 +5,7 @@ use axum::response::IntoResponse; use axum::response::Response; use axum_extra::extract::cookie::Cookie; use axum_extra::extract::cookie; +use lib_humus::HumusView; use crate::DigResult; use crate::IpResult; @@ -12,7 +13,6 @@ use crate::config::DnsResolverConfig; use crate::settings::QuerySettings; use crate::settings::ResponseFormat; -use crate::mycelium::MycView; #[derive(serde::Serialize, Clone)] #[serde(untagged)] @@ -28,7 +28,7 @@ pub enum View { NotFound, } -impl MycView for View { +impl HumusView for View { fn get_template_name(&self) -> String { match self { View::Asn{..} => "asn",