diff --git a/.gitignore b/.gitignore
index a008caf..dfdee31 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
Cargo.lock
example
*.diff
+
diff --git a/Cargo.toml b/Cargo.toml
index 0caf2ea..c1fed7a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/src/examples/plugins/plugin.lua b/src/examples/plugins/plugin.lua
new file mode 100644
index 0000000..dff80fe
--- /dev/null
+++ b/src/examples/plugins/plugin.lua
@@ -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
\ No newline at end of file
diff --git a/src/examples/plugins/todo.md b/src/examples/plugins/todo.md
new file mode 100644
index 0000000..42f8e8b
--- /dev/null
+++ b/src/examples/plugins/todo.md
@@ -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
\ No newline at end of file
diff --git a/src/examples/www/index.html b/src/examples/www/index.html
new file mode 100644
index 0000000..01d0119
--- /dev/null
+++ b/src/examples/www/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ TestDoc
+
+
+ If this is there the server worked!
+
+
+
\ No newline at end of file
diff --git a/src/lua_plugin.rs b/src/lua_plugin.rs
new file mode 100644
index 0000000..f3dabef
--- /dev/null
+++ b/src/lua_plugin.rs
@@ -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);
+
+impl UserData for LuaRouter {
+ fn add_methods>(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, router: Arc) -> 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(())
+}
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 4136c4a..73f5155 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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,
-}
+ /// Lua plugin script to load
+ #[arg(long)]
+ lua_plugin: Option,
+
+ /// Arguments passed to the Lua plugin (can be used multiple times)
+ #[arg(long)]
+ lua_arg: Vec,
+}
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);
+ }
}
diff --git a/src/response.rs b/src/response.rs
index d171dce..006334e 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -12,9 +12,9 @@ use tiny_http::{Header, Response, StatusCode};
use tinytemplate::TinyTemplate;
use serde::Serialize;
-type BoxedResponse = Response>>;
+pub type BoxedResponse = Response>>;
-fn content_type(value: &str) -> Header {
+pub fn content_type(value: &str) -> Header {
Header::from_bytes("Content-Type", value).unwrap()
}
diff --git a/src/router.rs b/src/router.rs
index 5c6708b..5e2f1e2 100644
--- a/src/router.rs
+++ b/src/router.rs
@@ -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>,
+}
+
+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")
+ }
+ }
+ }
+}
diff --git a/src/server.rs b/src/server.rs
index 3b83711..fbb7069 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -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,
+ router: Arc,
port: u16,
- threads: usize
+ threads: usize,
) -> Result<(), Box> {
-
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(())
}
-