codename mycelium became lib-humus

This commit is contained in:
Slatian 2023-10-30 01:22:27 +01:00
parent 912a119361
commit f2e9e36e99
12 changed files with 27 additions and 528 deletions

14
Cargo.lock generated
View File

@ -492,6 +492,7 @@ dependencies = [
"governor", "governor",
"idna 0.3.0", "idna 0.3.0",
"lazy_static", "lazy_static",
"lib-humus",
"maxminddb", "maxminddb",
"mime", "mime",
"parking_lot", "parking_lot",
@ -1011,6 +1012,19 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 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]] [[package]]
name = "libc" name = "libc"
version = "0.2.149" version = "0.2.149"

View File

@ -7,6 +7,8 @@ authors = ["Slatian <baschdel@disroot.org>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [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 = { version = "0.6", features = ["macros", "headers"] }
axum-extra = { version = "0.7", features = ["cookie"] } axum-extra = { version = "0.7", features = ["cookie"] }
axum-client-ip = "0.4" axum-client-ip = "0.4"

View File

@ -34,11 +34,14 @@ use std::collections::HashMap;
use std::net::IpAddr; use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
use lib_humus::TemplateEngineLoader;
use lib_humus::read_toml_from_file;
use lib_humus::HumusEngine;
mod config; mod config;
mod geoip; mod geoip;
mod idna; mod idna;
mod ipinfo; mod ipinfo;
mod mycelium;
mod ratelimit; mod ratelimit;
mod settings; mod settings;
mod simple_dns; mod simple_dns;
@ -51,15 +54,12 @@ use crate::geoip::{
LocationResult, LocationResult,
}; };
use crate::idna::IdnaName; 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::simple_dns::DnsLookupResult;
use crate::settings::*; use crate::settings::*;
use crate::view::View; use crate::view::View;
use crate::ipinfo::{AddressCast,AddressInfo,AddressScope}; use crate::ipinfo::{AddressCast,AddressInfo,AddressScope};
type TemplatingEngine = MycEngine<View,QuerySettings,ResponseFormat>; type TemplatingEngine = HumusEngine<View,QuerySettings,ResponseFormat>;
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
pub struct SettingsQuery { pub struct SettingsQuery {

View File

@ -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<Table>,
}
#[derive(Clone)]
pub struct MycEngine<V, S, F>
where V: MycView<S, F>, S: MycQuerySettings<F>, F: MycFormat {
pub tera: Tera,
pub template_config: Option<Table>,
phantom_view: PhantomData<V>,
phantom_settings: PhantomData<S>,
phantom_format: PhantomData<F>,
}
impl<V, S, F> MycEngine<V, S, F>
where V: MycView<S, F>, S: MycQuerySettings<F>, F: MycFormat {
pub fn new(tera: Tera, template_config: Option<Table>) -> 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::<Full<Bytes>>::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<V, S, F> From<MycProtoEngine> for MycEngine<V, S, F>
where V: MycView<S, F>, S: MycQuerySettings<F>, F: MycFormat {
fn from(e: MycProtoEngine) -> Self {
Self::new(e.tera, e.template_config)
}
}
impl<V, S, F> From<MycEngine<V, S, F>> for MycProtoEngine
where V: MycView<S, F>, S: MycQuerySettings<F>, F: MycFormat {
fn from(e: MycEngine<V, S, F>) -> Self {
Self {
tera: e.tera,
template_config: e.template_config,
}
}
}

View File

@ -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<Mime> {
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<Self>;
}
// 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<Self> {
match name {
"html" => Some(Self::Html),
"text" => Some(Self::Text),
"json" => Some(Self::Json),
_ => None,
}
}
}

View File

@ -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;

View File

@ -1,16 +0,0 @@
use tera::Context;
use crate::mycelium::MycFormat;
pub trait MycQuerySettings<F>
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;
}

View File

@ -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<String>,
}
impl TemplateEngineLoader {
pub fn new(
template_location: String,
extra_config_location: Option<String>
) -> Self {
Self {
template_location: template_location,
extra_config_location: extra_config_location,
}
}
pub fn cli_template_location(mut self, location: Option<String>) -> Self {
if let Some(location) = location {
self.template_location = location;
}
self
}
pub fn cli_extra_config_location(mut self, location: Option<String>) -> 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<MycProtoEngine,TemplateEngineLoaderError> {
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}"),
}
}
}

View File

@ -1,42 +0,0 @@
use serde::Deserialize;
use std::fmt;
use std::fs;
pub fn read_toml_from_file<T>(path: &String) -> Result<T,TomlError>
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}"),
}
}
}

View File

@ -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<S,F>: Serialize + Sized
where S: MycQuerySettings<F>, 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<String> { 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()
}
}

View File

@ -1,8 +1,9 @@
use serde::{Deserialize,Serialize}; use serde::{Deserialize,Serialize};
use lib_humus::HtmlTextJsonFormat;
use lib_humus::HumusQuerySettings;
use std::sync::Arc; use std::sync::Arc;
use crate::mycelium::HtmlTextJsonFormat;
use crate::mycelium::MycQuerySettings;
/* Response format */ /* Response format */
@ -25,7 +26,7 @@ pub struct Selectable {
pub weight: i32, pub weight: i32,
} }
impl MycQuerySettings<ResponseFormat> for QuerySettings { impl HumusQuerySettings<ResponseFormat> for QuerySettings {
fn initalize_template_context(&self, context: &mut tera::Context) { fn initalize_template_context(&self, context: &mut tera::Context) {
context.insert("language", &self.lang); context.insert("language", &self.lang);

View File

@ -5,6 +5,7 @@ use axum::response::IntoResponse;
use axum::response::Response; use axum::response::Response;
use axum_extra::extract::cookie::Cookie; use axum_extra::extract::cookie::Cookie;
use axum_extra::extract::cookie; use axum_extra::extract::cookie;
use lib_humus::HumusView;
use crate::DigResult; use crate::DigResult;
use crate::IpResult; use crate::IpResult;
@ -12,7 +13,6 @@ use crate::config::DnsResolverConfig;
use crate::settings::QuerySettings; use crate::settings::QuerySettings;
use crate::settings::ResponseFormat; use crate::settings::ResponseFormat;
use crate::mycelium::MycView;
#[derive(serde::Serialize, Clone)] #[derive(serde::Serialize, Clone)]
#[serde(untagged)] #[serde(untagged)]
@ -28,7 +28,7 @@ pub enum View {
NotFound, NotFound,
} }
impl MycView<QuerySettings, ResponseFormat> for View { impl HumusView<QuerySettings, ResponseFormat> for View {
fn get_template_name(&self) -> String { fn get_template_name(&self) -> String {
match self { match self {
View::Asn{..} => "asn", View::Asn{..} => "asn",