initial implementation of mlua for loading plugins, see todo.md in examples
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
Cargo.lock
|
||||
example
|
||||
*.diff
|
||||
|
||||
|
||||
+2
-1
@@ -1,13 +1,14 @@
|
||||
[package]
|
||||
name = "http-rs"
|
||||
version = "0.1.0"
|
||||
author="rattatwinko"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = {version = "4.5.55", features = ["derive"]}
|
||||
logger-rust = "0.2.12"
|
||||
mime_guess = "2.0.5"
|
||||
mlua = {version="0.11.6", features = ["lua55", "vendored"]}
|
||||
once_cell = "1.21.4"
|
||||
serde = { version = "1.0.228", features = ["derive"]}
|
||||
threadpool = "1.8.1"
|
||||
tiny_http = "0.12.0"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- example plugin for http-rs ; using current api
|
||||
|
||||
print("type(router) = " .. type(router))
|
||||
print("router = ", router)
|
||||
|
||||
router:add_file_server("/uploads", "./uploaded_files")
|
||||
|
||||
router:add_static("/api/status", 200, '{"status":"ok","version":"1.0"}', "application/json")
|
||||
|
||||
print("Lua plugin loaded with the following arguments:")
|
||||
for i, v in ipairs(args) do
|
||||
print(" arg[" .. i .. "] = " .. tostring(v))
|
||||
end
|
||||
|
||||
if args[1] == "debug" then
|
||||
router:add_static("/debug", 200, "Debug mode active", "text/plain")
|
||||
print("Debug endpoint added at /debug")
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# mlua-plugin-addition todo list
|
||||
|
||||
## implemented ("working")
|
||||
|
||||
- [x] **cli args**
|
||||
`--lua-plugin` and `--lua-arg` (multiple) added with `clap`
|
||||
|
||||
- [x] **lua scripting**
|
||||
`lua_plugin.rs` with `LuaRouter` userdata
|
||||
|
||||
- [x] **file server mounting from lua**
|
||||
`router:add_file_server(prefix, root)` – paths relative to plugin file
|
||||
|
||||
- [x] **route registration from lua**
|
||||
`router:add_static(path, status, body, content_type)`
|
||||
|
||||
- [x] **fallback static file server**
|
||||
Original `resolve_fs` logic still works for non lua routes
|
||||
|
||||
- [x] **pass cli args to lua**
|
||||
Accessible as `args` table inside Lua scripts.
|
||||
|
||||
## fixing implementing needed
|
||||
|
||||
- [ ] **better error messages from Lua**
|
||||
log something at all. write a custom logger ; and use logger_rs for it
|
||||
|
||||
- [ ] **hot reload of lua plugins**
|
||||
i want this
|
||||
|
||||
- [ ] **more lua api functions**
|
||||
very bare bones right now
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TestDoc</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>If this is there the server worked!</h1>
|
||||
<script>
|
||||
var msg = "This is js!";
|
||||
console.log(msg);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use logger_rust::*;
|
||||
use mlua::{Lua, Table, UserData, UserDataMethods};
|
||||
use crate::router::Router;
|
||||
|
||||
// just use userdata
|
||||
pub struct LuaRouter(pub Arc<Router>);
|
||||
|
||||
impl UserData for LuaRouter {
|
||||
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
|
||||
// lua example -> router:add_file_server(prefix, root)
|
||||
methods.add_method("add_file_server", |lua, this, (prefix, root): (String, String)| {
|
||||
// get lua dir ; dont panic and fuck up.
|
||||
let plugin_dir: String = lua.globals().get("_PLUGIN_DIR")?;
|
||||
let plugin_dir = PathBuf::from(plugin_dir);
|
||||
let root_path = PathBuf::from(&root);
|
||||
let resolved_root = if root_path.is_relative() {
|
||||
plugin_dir.join(root_path)
|
||||
} else {
|
||||
root_path
|
||||
};
|
||||
this.0.add_file_server(prefix, resolved_root)
|
||||
.map_err(mlua::Error::external)
|
||||
});
|
||||
|
||||
// lua method example -> router:add_static(path, status, body, content_type)
|
||||
methods.add_method("add_static", |_, this, (path, status, body, content_type): (String, u16, String, String)| {
|
||||
this.0.add_static(path, status, body, content_type)
|
||||
.map_err(mlua::Error::external)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// load a plugin and register it
|
||||
pub fn load_plugin(plugin_path: &Path, args: Vec<String>, router: Arc<Router>) -> Result<(), String> {
|
||||
let lua = Lua::new();
|
||||
|
||||
// we need the dir from the plugin so we can make other methods use that instead of just the binary path
|
||||
// using this we dont fuck up when trying to load a file, cause that panics and aborts xD
|
||||
let plugin_dir = plugin_path.parent().unwrap_or(Path::new(".")).to_path_buf();
|
||||
lua.globals().set("_PLUGIN_DIR", plugin_dir.to_string_lossy().to_string())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// hacky workaround for getting plugins working in their respective directory; if you like you can see this as a cwd
|
||||
let package_path = format!("{}/?.lua;{}/?/init.lua", plugin_dir.display(), plugin_dir.display());
|
||||
let globals = lua.globals();
|
||||
let package: Table = globals.get("package").map_err(|e| e.to_string())?;
|
||||
let current_path: String = package.get("path").unwrap_or_default();
|
||||
let new_path = format!("{};{}", package_path, current_path);
|
||||
package.set("path", new_path).map_err(|e| e.to_string())?;
|
||||
|
||||
// pass cli args from rust to lua as a Lua table
|
||||
let lua_args = lua.create_table().map_err(|e| e.to_string())?;
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
lua_args.set(i + 1, arg.as_str()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
lua.globals().set("args", lua_args).map_err(|e| e.to_string())?;
|
||||
|
||||
// create and set the router object
|
||||
let lua_router = LuaRouter(router);
|
||||
lua.globals().set("router", lua_router).map_err(|e| e.to_string())?;
|
||||
|
||||
// Execute the script
|
||||
let script = std::fs::read_to_string(plugin_path).map_err(|e| e.to_string())?;
|
||||
lua.load(&script).exec().map_err(|e| e.to_string())?;
|
||||
|
||||
log_info!("Lua plugin loaded successfully from {:?}", plugin_path);
|
||||
Ok(())
|
||||
}
|
||||
+27
-12
@@ -1,50 +1,65 @@
|
||||
mod server;
|
||||
mod router;
|
||||
mod response;
|
||||
mod lua_plugin;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use logger_rust::*;
|
||||
|
||||
use clap::Parser;
|
||||
use crate::router::Router;
|
||||
|
||||
// 1CA:b6e8ae4ca8acae66aa3dfd3f0a377e91d041e4d7 ; added clap as a parser, its way cleaner
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "http-rs")]
|
||||
#[command(about = "http-rs webserver")]
|
||||
#[command(about = "http-rs webserver with Lua plugin support")]
|
||||
struct Arguments {
|
||||
/// port where we should serve on
|
||||
/// Port where the server should listen
|
||||
#[arg(short, long, default_value_t = 8080)]
|
||||
port : u16,
|
||||
port: u16,
|
||||
|
||||
/// where we should serve from
|
||||
/// Root directory to serve static files from
|
||||
#[arg(short, long, default_value = ".")]
|
||||
root: PathBuf,
|
||||
|
||||
/// number of threads to run on
|
||||
/// Number of worker threads
|
||||
#[arg(short, long)]
|
||||
threads: Option<usize>,
|
||||
}
|
||||
|
||||
/// Lua plugin script to load
|
||||
#[arg(long)]
|
||||
lua_plugin: Option<PathBuf>,
|
||||
|
||||
/// Arguments passed to the Lua plugin (can be used multiple times)
|
||||
#[arg(long)]
|
||||
lua_arg: Vec<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Arguments::parse();
|
||||
let root = Arc::new(args.root);
|
||||
|
||||
if !root.exists() || !root.is_dir() {
|
||||
log_error!("Root {} isnt a valid directory", root.display());
|
||||
log_error!("Root '{}' is not a valid directory", root.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// moved num_cpus from server.rs to main.rs
|
||||
let threads = args.threads.unwrap_or_else(|| {
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get() * 2)
|
||||
.unwrap_or(8)
|
||||
});
|
||||
|
||||
if let Err(_) = server::run(root, args.port, threads) {
|
||||
log_error!("port {} in use!", args.port);
|
||||
let router = Arc::new(Router::new((*root).clone()));
|
||||
|
||||
if let Some(plugin_path) = args.lua_plugin {
|
||||
if let Err(e) = lua_plugin::load_plugin(&plugin_path, args.lua_arg, router.clone()) {
|
||||
log_error!("Failed to load Lua plugin: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(_) = server::run(router, args.port, threads) {
|
||||
log_error!("Port {} is already in use", args.port);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,9 +12,9 @@ use tiny_http::{Header, Response, StatusCode};
|
||||
use tinytemplate::TinyTemplate;
|
||||
use serde::Serialize;
|
||||
|
||||
type BoxedResponse = Response<Cursor<Vec<u8>>>;
|
||||
pub type BoxedResponse = Response<Cursor<Vec<u8>>>;
|
||||
|
||||
fn content_type(value: &str) -> Header {
|
||||
pub fn content_type(value: &str) -> Header {
|
||||
Header::from_bytes("Content-Type", value).unwrap()
|
||||
}
|
||||
|
||||
|
||||
+102
-15
@@ -1,8 +1,8 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
use logger_rust::*;
|
||||
|
||||
// Fixed from version a671182b07da0c22ed29e66f87faa3738fbdca64
|
||||
// onwards
|
||||
use tiny_http::StatusCode;
|
||||
use crate::response;
|
||||
|
||||
pub enum ResolveResult {
|
||||
Found(PathBuf),
|
||||
@@ -10,40 +10,127 @@ pub enum ResolveResult {
|
||||
Traversal,
|
||||
}
|
||||
|
||||
pub fn resolve(root: &Path, url_path: &str) -> ResolveResult {
|
||||
pub fn resolve_fs(root: &Path, url_path: &str) -> ResolveResult {
|
||||
let rel = url_path.trim_start_matches('/');
|
||||
let rel = if rel.is_empty() { "index.html" } else { rel };
|
||||
|
||||
// join once
|
||||
let mut joined = root.join(rel);
|
||||
|
||||
// if directory look for index
|
||||
// this is shitty logic, TODO: Improve this snippet
|
||||
if joined.is_dir() {
|
||||
let html = joined.join("index.html");
|
||||
let htm = joined.join("index.htm");
|
||||
let htm = joined.join("index.htm");
|
||||
if html.exists() {
|
||||
joined = html;
|
||||
} else if htm.exists() {
|
||||
joined = htm;
|
||||
} else {
|
||||
return ResolveResult::NoIndex; // no index found
|
||||
return ResolveResult::NoIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// nono no escape
|
||||
let canonical_root = root.canonicalize().ok();
|
||||
let canonical_file = joined.canonicalize().ok();
|
||||
|
||||
// cleanup 1CA:b6e8ae4ca8acae66aa3dfd3f0a377e91d041e4d7
|
||||
match (canonical_root, canonical_file) {
|
||||
(Some(root), Some(file))
|
||||
if file.starts_with(&root) => {
|
||||
ResolveResult::Found(file)
|
||||
}
|
||||
(Some(root), Some(file)) if file.starts_with(&root) => ResolveResult::Found(file),
|
||||
_ => {
|
||||
log_warn!("ResolveResult::Traversal");
|
||||
ResolveResult::Traversal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum LuaRoute {
|
||||
FileServer { prefix: String, root: PathBuf },
|
||||
StaticResponse { prefix: String, status: u16, body: String, content_type: String },
|
||||
}
|
||||
|
||||
pub struct Router {
|
||||
root: PathBuf,
|
||||
lua_routes: RwLock<Vec<LuaRoute>>,
|
||||
}
|
||||
|
||||
impl Router {
|
||||
pub fn new(root: PathBuf) -> Self {
|
||||
Self {
|
||||
root,
|
||||
lua_routes: RwLock::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_file_server(&self, prefix: String, root: PathBuf) -> Result<(), String> {
|
||||
if !prefix.starts_with('/') {
|
||||
return Err("Prefix must start with '/'".to_string());
|
||||
}
|
||||
let canonical = root.canonicalize().map_err(|e| e.to_string())?;
|
||||
if !canonical.exists() || !canonical.is_dir() {
|
||||
return Err(format!("Root '{}' is not a valid directory", canonical.display()));
|
||||
}
|
||||
let mut routes = self.lua_routes.write().unwrap();
|
||||
routes.push(LuaRoute::FileServer { prefix, root: canonical });
|
||||
// sort by longest prefix first
|
||||
routes.sort_by(|a, b| {
|
||||
let len_a = match a { LuaRoute::FileServer { prefix, .. } => prefix.len(), LuaRoute::StaticResponse { prefix, .. } => prefix.len() };
|
||||
let len_b = match b { LuaRoute::FileServer { prefix, .. } => prefix.len(), LuaRoute::StaticResponse { prefix, .. } => prefix.len() };
|
||||
len_b.cmp(&len_a)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_static(&self, path: String, status: u16, body: String, content_type: String) -> Result<(), String> {
|
||||
if !path.starts_with('/') {
|
||||
return Err("Path must start with '/'".to_string());
|
||||
}
|
||||
let mut routes = self.lua_routes.write().unwrap();
|
||||
routes.push(LuaRoute::StaticResponse { prefix: path.clone(), status, body, content_type });
|
||||
routes.sort_by(|a, b| {
|
||||
let len_a = match a { LuaRoute::FileServer { prefix, .. } => prefix.len(), LuaRoute::StaticResponse { prefix, .. } => prefix.len() };
|
||||
let len_b = match b { LuaRoute::FileServer { prefix, .. } => prefix.len(), LuaRoute::StaticResponse { prefix, .. } => prefix.len() };
|
||||
len_b.cmp(&len_a)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resolve(&self, url_path: &str) -> response::BoxedResponse {
|
||||
let routes = self.lua_routes.read().unwrap();
|
||||
for route in routes.iter() {
|
||||
let (prefix, matched) = match route {
|
||||
LuaRoute::FileServer { prefix, .. } => (prefix, url_path.starts_with(prefix)),
|
||||
LuaRoute::StaticResponse { prefix, .. } => (prefix, url_path == prefix),
|
||||
};
|
||||
if matched {
|
||||
let remaining = &url_path[prefix.len()..];
|
||||
return match route {
|
||||
LuaRoute::FileServer { root, .. } => {
|
||||
let rel = remaining.trim_start_matches('/');
|
||||
let full_path = if rel.is_empty() {
|
||||
root.join("index.html")
|
||||
} else {
|
||||
root.join(rel)
|
||||
};
|
||||
response::serve_file(&full_path)
|
||||
}
|
||||
LuaRoute::StaticResponse { status, body, content_type, .. } => {
|
||||
let mut resp = tiny_http::Response::from_string(body.clone());
|
||||
resp = resp.with_status_code(StatusCode(*status));
|
||||
resp = resp.with_header(response::content_type(content_type));
|
||||
resp
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
match resolve_fs(&self.root, url_path) {
|
||||
ResolveResult::Found(path) => response::serve_file(&path),
|
||||
ResolveResult::NoIndex => {
|
||||
log_warn!("No index file for: {}", url_path);
|
||||
response::not_found("No index file found")
|
||||
}
|
||||
ResolveResult::Traversal => {
|
||||
log_warn!("Rejected path traversal attempt: {}", url_path);
|
||||
response::forbidden("Access denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-21
@@ -1,42 +1,28 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use logger_rust::*;
|
||||
use threadpool::ThreadPool;
|
||||
use tiny_http::Server;
|
||||
|
||||
use crate::router;
|
||||
use crate::response;
|
||||
use crate::router::Router;
|
||||
|
||||
pub fn run(
|
||||
root: Arc<PathBuf>,
|
||||
router: Arc<Router>,
|
||||
port: u16,
|
||||
threads: usize
|
||||
threads: usize,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
|
||||
let addr = format!("0.0.0.0:{}", port);
|
||||
let server = Arc::new(Server::http(&addr)?);
|
||||
let pool = ThreadPool::new(threads);
|
||||
|
||||
log_info!("Serving '{}' on http://{}", root.display(), addr);
|
||||
log_info!("Serving on http://{}", addr);
|
||||
|
||||
for request in server.incoming_requests() {
|
||||
let root = Arc::clone(&root);
|
||||
|
||||
let router = Arc::clone(&router);
|
||||
pool.execute(move || {
|
||||
let url = request.url().to_owned();
|
||||
log_info!("{} {}", request.method(), url);
|
||||
|
||||
let resp = match router::resolve(&root, &url) {
|
||||
router::ResolveResult::Found(path) => response::serve_file(&path),
|
||||
router::ResolveResult::NoIndex => {
|
||||
log_warn!("No index file for: {}", url);
|
||||
response::not_found("No index file found")
|
||||
}
|
||||
router::ResolveResult::Traversal => {
|
||||
log_warn!("Rejected path traversal attempt: {}", url);
|
||||
response::forbidden("Access denied")
|
||||
}
|
||||
};
|
||||
let resp = router.resolve(&url);
|
||||
|
||||
if let Err(e) = request.respond(resp) {
|
||||
log_error!("Failed to send response: {}", e);
|
||||
@@ -46,4 +32,3 @@ pub fn run(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user