diff --git a/Cargo.lock b/Cargo.lock index e192997018ec2c92fb1846f120a64edf5a443b0a..4734d0d37f5a9abc99d23d8db0c59ec924b5dbf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,6 +255,50 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", + "humansize", + "num-traits", + "percent-encoding", +] + +[[package]] +name = "askama_derive" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ccf09143e56923c12e027b83a9553210a3c58322ed8419a53461b14a4dccd85" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn 2.0.39", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262eb9cf7be51269c5f2951eeda9ccd14d6934e437457f47b4f066bf55a6770d" +dependencies = [ + "nom", +] + [[package]] name = "async-attributes" version = "1.1.2" @@ -511,6 +555,15 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "basic-toml" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f2139706359229bfa8f19142ac1155b4b80beafb7a60471ac5dd109d4a19778" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "1.3.3" @@ -1300,6 +1353,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "0.14.27" @@ -1496,6 +1558,12 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1599,6 +1667,22 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1687,6 +1771,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2288,6 +2382,7 @@ version = "0.3.0" dependencies = [ "actix-web", "anyhow", + "askama", "colored", "fancy-regex", "home", @@ -3034,6 +3129,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.14" diff --git a/Cargo.toml b/Cargo.toml index 446dc9c4e0efdc458036ba2755696d7de2995689..05c93d878eaa16493fd3c3ae1d5c02d8b60b6a89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,4 @@ rhai = { version = "1.16.3", features = ["serde_json", "serde"] } mongodb = { version = "2.8.0", features = ["sync"], default-features = false } pickledb = { version = "0.5.1", features = ["json", "bincode", "cbor", "yaml"] } redis = "0.24.0" +askama = "0.12.1" diff --git a/app.routes b/app.routes index e504b7c80119f0ac7c9fb75fdb3b394af60943f4..7aeb3c83586161884fdf2f632287e193883c6570 100644 --- a/app.routes +++ b/app.routes @@ -129,7 +129,7 @@ test/loadfile() { // remove to test 404 route * { - text("this is a wildcard route\ncurrently on: " + path) + text("this is a wildcard route\ncurrently on: " + request.path) } 404 { diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000000000000000000000000000000000000..c5a12abc49bd09ccb402d6a56859fa515461c655 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,41 @@ +use actix_web::http::StatusCode; +use regex::{Captures, Regex}; +use rhai::{Engine, ParseError, AST}; + +pub fn rm_first(s: &str) -> &str { + let mut chars = s.chars(); + chars.next(); + chars.as_str() +} + +pub fn convert_to_format(input: &str) -> String { + let re = Regex::new(r"\.(\w+)").unwrap(); + format!("_route_{}", re.replace_all(&input.replace("/", "_"), |captures: &Captures| format!("__d{}", rm_first(&captures[0])))) +} + +pub fn route_to_fn(input: &str) -> String { + let re = Regex::new(r#"\{([^{}\s]+)\}"#).unwrap(); + let re_dot = Regex::new(r"\.(\w+)").unwrap(); + + let result = re.replace_all(&input, |captures: ®ex::Captures| { + let content = captures.get(1).map_or("", |m| m.as_str()); + format!("_arg_{content}") + }); + + format!( + "_route_fmt_{}", + re_dot.replace_all(&result.replace("/", "_"), |captures: &Captures| format!("__d{}", rm_first(&captures[0]))) + ) +} + +pub fn convert_status(code: i64) -> StatusCode { + let u16_code = code as u16; + StatusCode::from_u16(u16_code).unwrap_or(StatusCode::OK) +} + +pub fn error(engine: &Engine, path: &str, err: ParseError) -> AST { + match engine.compile(format!("fn {path}(){{text(\"error reading script file: {err}\")}}")) { + Ok(ast) => ast, + Err(_) => Default::default(), + } +} diff --git a/src/main.rs b/src/main.rs index d50554ec43758f932d7ede61a6345152128c97cb..03ea491efe45f4f02355903fd46a4b89aafa1252 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,9 @@ mod config; mod database; mod file; +mod helpers; +use askama::Template; use config::structs::Config; use lazy_static::lazy_static; use macros_rs::{crashln, str, string, ternary}; @@ -15,7 +17,7 @@ use std::{cell::RefCell, collections::BTreeMap, env, fs, sync::Arc}; use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; use tracing_subscriber::{filter::LevelFilter, prelude::*}; -use rhai::{packages::Package, plugin::*, serde::to_dynamic, Array, Dynamic, Engine, FnNamespace, Map, ParseError, Scope, AST}; +use rhai::{packages::Package, plugin::*, serde::to_dynamic, Array, Dynamic, Engine, FnNamespace, Map, Scope}; use rhai_fs::FilesystemPackage; use rhai_url::UrlPackage; @@ -33,6 +35,22 @@ use actix_web::{ App, HttpRequest, HttpResponse, HttpServer, Responder, }; +#[derive(Template)] +#[template(path = "error.html")] +struct ServerError { + error: String, + context: Vec<(String, String)>, +} + +#[derive(Template)] +#[template(path = "message.html")] +struct Message<'a> { + code: u16, + note: &'a str, + error: &'a str, + message: String, +} + // convert to peg lazy_static! { static ref R_INDEX: Result = Regex::new(r"index\s*\{"); @@ -43,44 +61,6 @@ lazy_static! { static ref R_SLASH: Result = Regex::new(r"(?m)\/(?=.*\((.*?)\)\s*\{[^{]*$)"); } -fn rm_first(s: &str) -> &str { - let mut chars = s.chars(); - chars.next(); - chars.as_str() -} - -fn convert_to_format(input: &str) -> String { - let re = Regex::new(r"\.(\w+)").unwrap(); - format!("_route_{}", re.replace_all(&input.replace("/", "_"), |captures: &Captures| format!("__d{}", rm_first(&captures[0])))) -} - -fn route_to_fn(input: &str) -> String { - let re = Regex::new(r#"\{([^{}\s]+)\}"#).unwrap(); - let re_dot = Regex::new(r"\.(\w+)").unwrap(); - - let result = re.replace_all(&input, |captures: ®ex::Captures| { - let content = captures.get(1).map_or("", |m| m.as_str()); - format!("_arg_{content}") - }); - - format!( - "_route_fmt_{}", - re_dot.replace_all(&result.replace("/", "_"), |captures: &Captures| format!("__d{}", rm_first(&captures[0]))) - ) -} - -fn convert_status(code: i64) -> StatusCode { - let u16_code = code as u16; - StatusCode::from_u16(u16_code).unwrap_or(StatusCode::OK) -} - -fn error(engine: &Engine, path: &str, err: ParseError) -> AST { - match engine.compile(format!("fn {path}(){{text(\"error reading script file: {err}\")}}")) { - Ok(ast) => ast, - Err(_) => Default::default(), - } -} - pub fn response(data: String, content_type: String, status_code: i64) -> (String, ContentType, StatusCode) { let content_type = match content_type.as_str() { "xml" => ContentType::xml(), @@ -94,7 +74,7 @@ pub fn response(data: String, content_type: String, status_code: i64) -> (String _ => ContentType::plaintext(), }; - (data, content_type, convert_status(status_code)) + (data, content_type, helpers::convert_status(status_code)) } fn match_route(route_template: &str, placeholders: &[&str], url: &str) -> Option> { @@ -164,11 +144,11 @@ mod default { #[export_module] mod status { - pub fn text(string: String, status: i64) -> (String, ContentType, StatusCode) { (string, ContentType::plaintext(), convert_status(status)) } - pub fn html(string: String, status: i64) -> (String, ContentType, StatusCode) { (string, ContentType::html(), convert_status(status)) } + pub fn text(string: String, status: i64) -> (String, ContentType, StatusCode) { (string, ContentType::plaintext(), helpers::convert_status(status)) } + pub fn html(string: String, status: i64) -> (String, ContentType, StatusCode) { (string, ContentType::html(), helpers::convert_status(status)) } pub fn json(object: Dynamic, status: i64) -> (String, ContentType, StatusCode) { match serde_json::to_string(&object) { - Ok(result) => (result, ContentType::json(), convert_status(status)), + Ok(result) => (result, ContentType::json(), helpers::convert_status(status)), Err(err) => (err.to_string(), ContentType::plaintext(), StatusCode::INTERNAL_SERVER_ERROR), } } @@ -805,6 +785,20 @@ mod http { #[get("{url:.*}")] async fn handler(url: Path, req: HttpRequest, config: Data) -> impl Responder { + macro_rules! send { + ($response:expr) => {{ + let (body, content_type, status_code) = $response; + tracing::info!( + method = string!(req.method()), + status = string!(status_code), + content = string!(content_type), + "request '{}'", + req.uri() + ); + return HttpResponse::build(status_code).content_type(content_type).body(body); + }}; + } + if url.as_str() == "favicon.ico" { return HttpResponse::Ok().body(""); } @@ -824,7 +818,7 @@ async fn handler(url: Path, req: HttpRequest, config: Data) -> i let path = match url.as_str() { "" => "_route_index".to_string(), - _ => convert_to_format(&url.clone()), + _ => helpers::convert_to_format(&url.clone()), }; fs_pkg.register_into_engine(&mut engine); @@ -947,7 +941,11 @@ async fn handler(url: Path, req: HttpRequest, config: Data) -> i routes.insert(string!(path.replace("_", "/")), vec![]); } - let result = R_DOT.as_ref().unwrap().replace_all(&result, |captures: &Captures| format!("__d{}", rm_first(&captures[0]))).to_string(); + let result = R_DOT + .as_ref() + .unwrap() + .replace_all(&result, |captures: &Captures| format!("__d{}", helpers::rm_first(&captures[0]))) + .to_string(); let result = R_FN.as_ref().unwrap().replace_all(&result, |captures: &Captures| format!("fn _route_{}", &captures[0])).to_string(); ternary!(has_wildcard, R_WILD.as_ref().unwrap().replace_all(&result, "fn _wildcard() {").to_string(), result) @@ -955,24 +953,16 @@ async fn handler(url: Path, req: HttpRequest, config: Data) -> i let mut ast = match engine.compile(&contents) { Ok(ast) => ast, - Err(err) => error(&engine, &path, err), + Err(err) => helpers::error(&engine, &path, err), }; ast.set_source(filename.to_string_lossy().to_string()); if url.clone() == "" && has_index { - let (body, content_type, status_code) = engine.call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, "_route_index", ()).unwrap(); - tracing::info!( - method = string!(req.method()), - status = string!(status_code), - content = string!(content_type), - "request '{}'", - req.uri() - ); - return HttpResponse::build(status_code).content_type(content_type).body(body); + send!(engine.call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, "_route_index", ()).unwrap()); }; - fn extract_context(contents: String, err: String) -> String { + fn extract_context(contents: String, err: String) -> Vec<(String, String)> { let re = Regex::new(r"line (\d+)").unwrap(); if let Some(captures) = re.captures(&err).unwrap() { @@ -985,14 +975,13 @@ async fn handler(url: Path, req: HttpRequest, config: Data) -> i return lines[start_line..end_line] .iter() .enumerate() - .map(|(i, line)| format!("
{:>4} | {}
", start_line + i + 1, line)) - .collect::>() - .join("\n"); + .map(|(i, line)| (format!("{:>4}", start_line + i + 1), line.to_string())) + .collect::>(); } } } - "".to_string() + vec![] } for (route, args) in routes { @@ -1000,341 +989,51 @@ async fn handler(url: Path, req: HttpRequest, config: Data) -> i let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect(); if url == route { - match engine.call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, convert_to_format(&url.clone()), ()) { - Ok(response) => { - let (body, content_type, status_code) = response; - tracing::info!( - method = string!(req.method()), - status = string!(status_code), - content = string!(content_type), - "request '{}'", - req.uri() - ); - return HttpResponse::build(status_code).content_type(content_type).body(body); - } + match engine.call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, helpers::convert_to_format(&url.clone()), ()) { + Ok(response) => send!(response), Err(err) => { - return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR).content_type(ContentType::html()).body(format!( - r#" - - - - 500: Internal Server Error -
-
-

- 500: Internal Server Error - {} -

- -
{}
-
-
- "#, - err.to_string().replace("\n", "
"), - extract_context(contents, err.to_string()) - )) + let body = ServerError { + error: err.to_string().replace("\n", "
"), + context: extract_context(contents, err.to_string()), + }; + + return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR).content_type(ContentType::html()).body(body.render().unwrap()); } } } match match_route(&route, &args, &url) { - Some(data) => match engine.call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, route_to_fn(&route), data) { - Ok(response) => { - let (body, content_type, status_code) = response; - tracing::info!( - method = string!(req.method()), - status = string!(status_code), - content = string!(content_type), - "request '{}'", - req.uri() - ); - return HttpResponse::build(status_code).content_type(content_type).body(body); + Some(data) => match engine.call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, helpers::route_to_fn(&route), data) { + Ok(response) => send!(response), + Err(err) => { + let body = ServerError { + error: err.to_string().replace("\n", "
"), + context: extract_context(contents, err.to_string()), + }; + + return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR).content_type(ContentType::html()).body(body.render().unwrap()); } - Err(err) => return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR).body(format!("Internal Server Error\n\n{err}\n\n{}", extract_context(contents, err.to_string()))), }, None => {} } } - let (body, content_type, status_code) = { - if has_wildcard || has_error_page { - engine - .call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, ternary!(has_wildcard, "_wildcard", "_route_error_404"), ()) - .unwrap() - } else { - eprintln!("Error reading script file: {}", filename.to_string_lossy()); - ( - format!("function not found.\ndid you create {url}()?\n\nyou can add * {{}} or 404 {{}} routes as well."), - ContentType::plaintext(), - StatusCode::NOT_FOUND, - ) - } - }; + if has_wildcard || has_error_page { + let (body, content_type, status_code) = engine + .call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, ternary!(has_wildcard, "_wildcard", "_route_error_404"), ()) + .unwrap(); - let status_code = ternary!(has_wildcard, status_code, StatusCode::NOT_FOUND); - tracing::info!( - method = string!(req.method()), - status = string!(status_code), - content = string!(content_type), - "request '{}'", - req.uri() - ); - return HttpResponse::build(status_code).content_type(content_type).body(body); + send!((body, content_type, ternary!(has_wildcard, status_code, StatusCode::NOT_FOUND))) + } else { + let body = Message { + error: "Function Not Found", + code: StatusCode::NOT_FOUND.as_u16(), + message: format!("Have you created the {url}() route?"), + note: "You can add * {} or 404 {} routes as well", + }; + + send!((body.render().unwrap(), ContentType::html(), StatusCode::NOT_FOUND)) + } } #[actix_web::main] diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000000000000000000000000000000000000..51c528699c7d6aa904f44482577df21205067713 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,137 @@ + + + + + 500: Internal Server Error +
+
+

+ 500: Internal Server Error + {{ error }} +

+
+ {% for (line, code) in context %} +
{{ line }} | {{ code }}
+ {% endfor %} +
+
+
+ diff --git a/templates/message.html b/templates/message.html new file mode 100644 index 0000000000000000000000000000000000000000..79cb9fb9c8cb2addd1e3e7c54aa857261b9d3021 --- /dev/null +++ b/templates/message.html @@ -0,0 +1,134 @@ + + + + + {{ code }} {{ error }} +
+
+

+ {{ code }}: {{ error }} + {{ message|safe }} +

+
{{ note|safe }}
+
+
+