aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock34
-rw-r--r--Cargo.toml7
-rw-r--r--README.md18
-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
8 files changed, 212 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f5fd4d1..1c772dd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 59492a0..3bf7535 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"] }
diff --git a/README.md b/README.md
index ee0f8e6..068c1fa 100644
--- a/README.md
+++ b/README.md
@@ -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, "[\"><'&]", {
+ ["&"] = "&amp;",
+ ["<"] = "&lt;",
+ [">"] = "&gt;",
+ ['"'] = "&quot;",
+ ["'"] = "&#39;",
+ }))
+end
+
+-- TODO: add API to build HTML tags
+
+return gitpad