diff options
author | Martin Fischer <martin@push-f.com> | 2022-07-30 20:17:25 +0200 |
---|---|---|
committer | Martin Fischer <martin@push-f.com> | 2022-07-30 22:21:23 +0200 |
commit | 56c071bb832304ba9a2ec67215b1a8ae40723840 (patch) | |
tree | 2282e1f0eafcf65e807f2e9120617401b705d75c | |
parent | a920436c3af249266ff907d15755c1291737905a (diff) |
implement lua scripting
Inspired by the Scribunto extension for MediaWiki.
-rw-r--r-- | Cargo.lock | 34 | ||||
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | README.md | 18 | ||||
-rw-r--r-- | src/error.rs | 1 | ||||
-rw-r--r-- | src/get_routes.rs | 28 | ||||
-rw-r--r-- | src/lua.rs | 103 | ||||
-rw-r--r-- | src/main.rs | 7 | ||||
-rw-r--r-- | src/static/api.lua | 15 |
8 files changed, 212 insertions, 1 deletions
@@ -26,6 +26,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] name = "bytes" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -227,6 +236,7 @@ dependencies = [ "multer", "percent-encoding", "pulldown-cmark", + "rlua", "serde", "sputnik", "tempdir", @@ -686,6 +696,30 @@ dependencies = [ ] [[package]] +name = "rlua" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b38117a836316ef62c02f6751e6d28e2eb53a1c35f0329427a9fb9c1c7b6a0" +dependencies = [ + "bitflags", + "bstr", + "libc", + "num-traits", + "rlua-lua54-sys", +] + +[[package]] +name = "rlua-lua54-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ae48797c3e76fb2c205fda8f30e28416a15b9fc1d649cc7cea9ff1fb9cf028" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] name = "ryu" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6,11 +6,15 @@ edition = "2018" license = "MIT" description = "a git web interface with editing and Markdown support" repository = "https://git.push-f.com/gitpad" -keywords = ["git", "wiki", "markdown"] +keywords = ["git", "wiki", "markdown", "lua"] categories = ["command-line-utilities"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["lua"] +lua = ["rlua"] + [dependencies] hyper = { version = "0.14.20", features=["http1", "server", "runtime", "stream"]} tokio = { version = "1", features=["rt-multi-thread", "macros"] } @@ -34,6 +38,7 @@ chrono = "0.4" multer = "2.0.3" mime_guess = "2.0" camino = "1" +rlua = { version = "0.19.4", optional = true } [target.'cfg(unix)'.dependencies] hyperlocal = { version = "0.8", features = ["server"] } @@ -24,6 +24,10 @@ Listening on http://127.0.0.1:8000 By default GitPad is in single-user mode, serving the branch pointed to by `HEAD`. +**WARNING**: The `lua` feature is enabled by default. Lua sandboxing is still +experimental, so giving users access who you don't trust might lead to a system +compromise. + ## Multi-user mode Multi-user mode requires you to set up a reverse-proxy that authenticates users @@ -76,6 +80,20 @@ name = "John Doe" email = "john@example.com" ``` +## Lua scripting + +Files can start with a shebang like `#!hello`, which will +interpret the following text with the `view` function returned +by in this case `bin/hello.lua`, e.g. + +```lua +function view(text) + return '<pre>' .. gitpad.html_escape(string.upper(text)) .. '</pre>' +end + +return {view=view} +``` + ## Contributing Feedback, bug reports and suggestions are welcome! diff --git a/src/error.rs b/src/error.rs index 3cf6832..08829a5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,6 +8,7 @@ use std::str::Utf8Error; use crate::HyperResponse; /// For convenience this enum also contains nonerroneous variants. +#[derive(Debug)] pub enum Error { /// A 400 bad request error. BadRequest(String), diff --git a/src/get_routes.rs b/src/get_routes.rs index 79b7cb5..5e65637 100644 --- a/src/get_routes.rs +++ b/src/get_routes.rs @@ -11,6 +11,7 @@ use hyper::http::request::Parts; use serde::Deserialize; use sputnik::html_escape; use sputnik::request::SputnikParts; +use std::fmt::Write; use std::str::from_utf8; use crate::action_links; @@ -80,6 +81,33 @@ fn view_blob<C: Controller>( match from_utf8(blob.content()) { Ok(text) => { + #[cfg(feature = "lua")] + if let Some(script) = super::lua::parse_shebang(text) { + match script.run(&ctx) { + Ok(output) => { + write!( + page.body, + "<div>rendered with <a href='{}'>{}</a></div>", + html_escape( + controller.build_url_path(&ctx.branch, &script.module_path()) + ), + html_escape(script.lua_module_name) + ); + page.body.push_str(&output); + } + Err(err) => { + write!( + page.body, + "<div class=error><a href='{}'>{}</a>: {err}</div>", + html_escape( + controller.build_url_path(&ctx.branch, &script.module_path()) + ), + html_escape(script.lua_module_name) + ); + } + }; + return Ok(page.into()); + } if let Some(renderer) = get_renderer(&ctx.path) { renderer(text, &mut page, RenderMode::View); } else { diff --git a/src/lua.rs b/src/lua.rs new file mode 100644 index 0000000..d9f1511 --- /dev/null +++ b/src/lua.rs @@ -0,0 +1,103 @@ +use std::fmt::Display; +use std::path::Path; +use std::str::from_utf8; + +use rlua::Function; +use rlua::HookTriggers; +use rlua::Lua; +use rlua::StdLib; +use rlua::Table; + +use crate::Context; + +pub struct Script<'a> { + pub lua_module_name: &'a str, + input: &'a str, +} + +pub fn parse_shebang(text: &str) -> Option<Script> { + if let Some(rest) = text.strip_prefix("#!") { + if let Some((lua_module_name, input)) = rest.split_once('\n') { + return Some(Script { + lua_module_name, + input, + }); + } + } + None +} + +pub enum ScriptError { + ModuleNotFound, + ModuleNotUtf8, + LuaError(rlua::Error), +} + +#[derive(Debug)] +struct TimeOutError; + +impl Display for TimeOutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("execution took too long") + } +} + +impl std::error::Error for TimeOutError {} + +impl<'a> Script<'a> { + pub fn module_path(&self) -> String { + format!("bin/{}.lua", self.lua_module_name) + } + + pub fn run(&self, ctx: &Context) -> Result<String, ScriptError> { + let filename = self.module_path(); + + let lua_entr = ctx + .branch_head() + .unwrap() + .tree() + .and_then(|tree| tree.get_path(Path::new(&filename))) + .map_err(|_| ScriptError::ModuleNotFound)?; + + let lua_blob = ctx.repo.find_blob(lua_entr.id()).unwrap(); + let lua_code = from_utf8(lua_blob.content()).map_err(|_| ScriptError::ModuleNotUtf8)?; + + let lua = Lua::new_with(StdLib::ALL_NO_DEBUG - StdLib::IO - StdLib::OS - StdLib::PACKAGE); + lua.set_hook( + HookTriggers { + every_nth_instruction: Some(10_000), + ..Default::default() + }, + |_ctx, _debug| Err(rlua::Error::external(TimeOutError)), + ); + lua.context(|ctx| { + ctx.globals() + .raw_set( + "gitpad", + ctx.load(include_str!("static/api.lua")) + .eval::<Table>() + .expect("error in api.lua"), + ) + .unwrap(); + + let module: Table = ctx.load(lua_code).eval().map_err(ScriptError::LuaError)?; + let view: Function = module.get("view").map_err(ScriptError::LuaError)?; + + view.call::<_, String>(self.input) + .map_err(ScriptError::LuaError) + }) + } +} + +impl Display for ScriptError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ScriptError::ModuleNotFound => write!(f, "module not found"), + ScriptError::ModuleNotUtf8 => write!(f, "module not valid UTF-8"), + ScriptError::LuaError(rlua::Error::CallbackError { cause, .. }) => { + write!(f, "{}", cause) + } + ScriptError::LuaError(err) => write!(f, "{}", err), + } + } +} diff --git a/src/main.rs b/src/main.rs index 8005892..9f30ef2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,6 +46,8 @@ mod diff; mod error; mod forms; mod get_routes; +#[cfg(feature = "lua")] +mod lua; mod origins; mod post_routes; mod shares; @@ -99,6 +101,11 @@ async fn main() { let repo = Repository::open_bare(repo_path) .expect("expected current directory to be a bare Git repository"); + #[cfg(feature = "lua")] + { + eprintln!("[warning] the Lua code sandboxing is experimental, write access might lead to a system compromise") + } + if args.multiuser { serve(repo_path, MultiUserController::new(&repo), args).await; } else { diff --git a/src/static/api.lua b/src/static/api.lua new file mode 100644 index 0000000..3ca9830 --- /dev/null +++ b/src/static/api.lua @@ -0,0 +1,15 @@ +local gitpad = {} -- gitpad namespace + +function gitpad.html_escape(s) + return (string.gsub(s, "[\"><'&]", { + ["&"] = "&", + ["<"] = "<", + [">"] = ">", + ['"'] = """, + ["'"] = "'", + })) +end + +-- TODO: add API to build HTML tags + +return gitpad |