mod config;
mod database;
mod globals;
mod helpers;
mod modules;
mod routes;
mod structs;
use helpers::prelude::*;
use modules::prelude::*;
use routes::prelude::*;
use structs::{config::*, template::*};
use mime::Mime;
use reqwest::blocking::Client as ReqwestClient;
use smartstring::alias::String as SmString;
use std::{collections::HashMap, fs};
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_subscriber::{filter::EnvFilter, prelude::*};
use rhai::{packages::Package, plugin::*, Dynamic, Engine, Map, Scope};
use rhai_fs::FilesystemPackage;
use rhai_url::UrlPackage;
use macros_rs::{
fmt::{crashln, string},
os::set_env_sync,
};
use actix_web::{
http::{header::ContentType, StatusCode},
web::{self, Data},
App, HttpRequest, HttpResponse, HttpServer, Responder,
};
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(),
"png" => ContentType::png(),
"html" => ContentType::html(),
"json" => ContentType::json(),
"jpeg" => ContentType::jpeg(),
"text" => ContentType::plaintext(),
"stream" => ContentType::octet_stream(),
"form" => ContentType::form_url_encoded(),
_ => ContentType::plaintext(),
};
(data, content_type, helpers::convert_status(status_code))
}
fn parse_bool(s: &str) -> bool {
match s.trim().to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => true,
_ => false,
}
}
fn parse_slash(s: &str) -> String {
let parts: Vec<&str> = s.splitn(3, '/').collect();
if parts.len() > 1 {
format!("{}/{}", parts[0], parts[1])
} else {
s.to_string()
}
}
pub fn proxy(url: String) -> (String, ContentType, StatusCode) {
let client = ReqwestClient::new();
let response = match client.get(url).send() {
Ok(res) => res,
Err(err) => return (err.to_string(), ContentType::plaintext(), StatusCode::GATEWAY_TIMEOUT),
};
let status = response.status();
let content_type = response.headers().get("Content-Type").unwrap().to_str().unwrap_or("text/plain").parse::<Mime>().unwrap();
if status.is_success() {
(response.text().unwrap(), ContentType(content_type), status)
} else {
(response.text().unwrap(), ContentType(content_type), status)
}
}
async fn handler(req: HttpRequest, config: Data<Config>) -> impl Responder {
let url = req.uri().to_string();
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);
}};
}
macro_rules! error {
($err:expr) => {{
let body = Message {
error: "Function Not Found",
code: StatusCode::NOT_FOUND.as_u16(),
message: format!("Have you created the <code>{url}</code> route?"),
note: "You can add <code>* {}</code> or <code>404 {}</code> routes as well",
};
tracing::error!(err = string!($err), "Error finding route");
send!((body.render().unwrap(), ContentType::html(), StatusCode::NOT_FOUND))
}};
}
let filename = &config.workers.get(0).unwrap();
let fs_pkg = FilesystemPackage::new();
let url_pkg = UrlPackage::new();
let json = exported_module!(json);
let http = exported_module!(http);
let exists = exported_module!(exists);
let mut engine = Engine::new();
let mut scope = Scope::new();
fs_pkg.register_into_engine(&mut engine);
url_pkg.register_into_engine(&mut engine);
engine.register_static_module("json", json.into());
engine.register_static_module("http", http.into());
engine.register_static_module("exists", exists.into());
if let Some(database) = &config.database {
if let Some(_) = &database.kv {
let kv = exported_module!(kv_db);
engine.register_static_module("kv", kv.into());
}
if let Some(_) = &database.mongo {
let mongo = exported_module!(mongo_db);
engine.register_static_module("mongo", mongo.into());
}
if let Some(_) = &database.redis {
let redis = exported_module!(redis_db);
engine.register_static_module("redis", redis.into());
}
}
#[derive(Clone)]
struct Request {
path: String,
url: String,
version: String,
query: String,
}
impl Request {
fn to_dynamic(&self) -> Dynamic {
let mut map = Map::new();
map.insert(SmString::from("path"), Dynamic::from(self.path.clone()));
map.insert(SmString::from("url"), Dynamic::from(self.url.clone()));
map.insert(SmString::from("version"), Dynamic::from(self.version.clone()));
map.insert(SmString::from("query"), Dynamic::from(self.query.clone()));
Dynamic::from(map)
}
}
let request = Request {
path: url.to_string(),
url: req.uri().to_string(),
version: format!("{:?}", req.version()),
query: req.query_string().to_string(),
};
scope.push("request", request.to_dynamic());
engine
.register_fn("cwd", cwd)
.register_fn("proxy", proxy)
.register_fn("response", response)
.register_fn("text", default::text)
.register_fn("json", default::json)
.register_fn("html", default::html)
.register_fn("text", status::text)
.register_fn("json", status::json)
.register_fn("html", status::html);
let contents = match fs::read_to_string(&filename) {
Ok(contents) => contents,
Err(err) => crashln!("Error reading script file: {}\n{}", filename.to_string_lossy(), err),
};
routes::parse::try_parse(&contents).await;
let (route, args) = match Route::get(&parse_slash(&url)).await {
Ok(route) => {
let mut matched_url = url.to_owned();
let cfg = match route.cfg {
Some(cfg) => cfg,
None => HashMap::new(),
};
for (item, val) in cfg {
match item.as_str() {
"wildcard" => {
if parse_slash(&url) == route.route && parse_bool(&val) {
matched_url = parse_slash(&url);
break;
}
}
_ => {}
}
}
match Route::get(&matched_url).await {
Ok(matched) => (matched, vec![]),
Err(err) => match Route::search_for(&matched_url).await {
Some(matched) => matched,
None => error!(err),
},
}
}
Err(err) => match Route::search_for(&url).await {
Some(matched) => matched,
None => error!(err),
},
};
let mut ast = match engine.compile(route.construct_fn()) {
Ok(ast) => ast,
Err(err) => helpers::error(&engine, &url, err),
};
ast.set_source(filename.to_string_lossy().to_string());
let fn_name = match route.fn_name.as_str() {
"/" => "/index",
name => name,
};
match engine.call_fn::<(String, ContentType, StatusCode)>(&mut scope, &ast, fn_name, args) {
Ok(response) => send!(response),
Err(err) => {
let body = ServerError {
error: err.to_string().replace("\n", "<br>"),
context: vec![],
};
return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR).content_type(ContentType::html()).body(body.render().unwrap());
}
};
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
set_env_sync!(RUST_LOG = "info");
globals::init();
let config = config::read();
let app = || App::new().app_data(Data::new(config::read())).default_service(web::to(handler));
let formatting_layer = BunyanFormattingLayer::new("server".into(), std::io::stdout)
.skip_fields(vec!["file", "line"].into_iter())
.expect("Unable to create logger");
tracing_subscriber::registry().with(EnvFilter::from_default_env()).with(JsonStorageLayer).with(formatting_layer).init();
tracing::info!(address = config.settings.address, port = config.settings.port, "server started");
HttpServer::new(app).bind(config.get_address()).unwrap().run().await
}