aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2022-07-30 20:17:25 +0200
committerMartin Fischer <martin@push-f.com>2022-07-30 22:21:23 +0200
commit56c071bb832304ba9a2ec67215b1a8ae40723840 (patch)
tree2282e1f0eafcf65e807f2e9120617401b705d75c /src
parenta920436c3af249266ff907d15755c1291737905a (diff)
implement lua scripting
Inspired by the Scribunto extension for MediaWiki.
Diffstat (limited to 'src')
-rw-r--r--src/error.rs1
-rw-r--r--src/get_routes.rs28
-rw-r--r--src/lua.rs103
-rw-r--r--src/main.rs7
-rw-r--r--src/static/api.lua15
5 files changed, 154 insertions, 0 deletions
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, "[\"><'&]", {
+ ["&"] = "&amp;",
+ ["<"] = "&lt;",
+ [">"] = "&gt;",
+ ['"'] = "&quot;",
+ ["'"] = "&#39;",
+ }))
+end
+
+-- TODO: add API to build HTML tags
+
+return gitpad