use hyper::HeaderMap;
use rhai::{AST, Dynamic, Engine, Map, Scope, serde::to_dynamic};
use serde_json::Value;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use crate::{
error::{ServerError, ServerResult},
middleware::middleware_response::MiddlewareResponse,
response::confine::canonical_dir,
types::BoxBody,
};
mod limits {
pub const MAX_CALL_LEVELS: usize = 64;
pub const MAX_STRING_SIZE: usize = 1_000_000;
pub const MAX_ARRAY_SIZE: usize = 100_000;
pub const MAX_MAP_SIZE: usize = 10_000;
}
#[derive(Clone)]
#[non_exhaustive]
pub struct MiddlewareHandler {
pub engine: Arc<Engine>,
pub file_path: String,
pub ast: AST,
pub confine_to: Option<PathBuf>,
}
impl MiddlewareHandler {
pub fn new(file_path: &str, max_operations: u64) -> ServerResult<Self> {
let path = Path::new(file_path);
if !path.exists() {
return Err(ServerError::MiddlewareMissing {
path: path.to_path_buf(),
});
}
let mut engine = Engine::new();
engine.set_max_operations(max_operations);
engine.set_max_call_levels(limits::MAX_CALL_LEVELS);
engine.set_max_string_size(limits::MAX_STRING_SIZE);
engine.set_max_array_size(limits::MAX_ARRAY_SIZE);
engine.set_max_map_size(limits::MAX_MAP_SIZE);
let ast =
engine
.compile_file(file_path.into())
.map_err(|e| ServerError::MiddlewareCompile {
path: path.to_path_buf(),
reason: e.to_string(),
})?;
let confine_to = path
.parent()
.and_then(|p| p.to_str())
.and_then(canonical_dir);
Ok(MiddlewareHandler {
engine: Arc::new(engine),
file_path: file_path.to_owned(),
ast,
confine_to,
})
}
pub async fn handle(
&self,
request_url_path: &str,
request_body_json_value: Option<&Value>,
request_headers: &HeaderMap,
cors_allow_credentials_origins: &[String],
) -> Option<Result<hyper::Response<BoxBody>, hyper::http::Error>> {
let mut scope = Scope::new();
scope.push("url_path", request_url_path.to_owned());
if let Some(request_body_json_value) = request_body_json_value {
match to_dynamic(request_body_json_value) {
Ok(body_dynamic) => {
scope.push("body", body_dynamic);
}
Err(err) => {
log::warn!(
"middleware `{}`: failed to convert request body to Rhai Dynamic: {}",
self.file_path,
err
);
return None;
}
}
}
let engine = Arc::clone(&self.engine);
let ast = self.ast.clone();
let eval_result = match tokio::task::spawn_blocking(move || {
engine.eval_ast_with_scope::<Dynamic>(&mut scope, &ast)
})
.await
{
Ok(result) => result,
Err(join_err) => {
log::warn!(
"middleware `{}`: evaluation task panicked or was cancelled: {}",
self.file_path,
join_err
);
return None;
}
};
let rhai_response = match eval_result {
Ok(v) => v,
Err(err) => {
log::warn!(
"middleware `{}`: script evaluation failed: {}",
self.file_path,
err
);
return None;
}
};
if !rhai_response.is_string() && !rhai_response.is_map() {
return None;
}
let middleware_response = MiddlewareResponse::new(
self.file_path.as_str(),
request_headers,
self.confine_to.as_deref(),
cors_allow_credentials_origins,
);
if let Some(x) = rhai_response.clone().try_cast::<String>() {
middleware_response.file_response(x.as_str()).await
} else if let Some(x) = rhai_response.try_cast::<Map>() {
if let Some(x) = x
.get("file_path")
.and_then(|x| x.clone().try_cast::<String>())
{
middleware_response.file_response(x.as_str()).await
} else if let Some(x) = x.get("json").and_then(|x| x.clone().try_cast::<String>()) {
middleware_response.json_response(x.as_str())
} else if let Some(x) = x.get("text").and_then(|x| x.clone().try_cast::<String>()) {
middleware_response.text_response(x.as_str())
} else {
None
}
} else {
None
}
}
}