From 0610fd49c9165178ecee40d5dd7f684e20c99b0f Mon Sep 17 00:00:00 2001 From: finga Date: Sun, 21 Mar 2021 15:51:58 +0100 Subject: [PATCH] Replace command parameters with values To create a minimalistic parser, nom is used to identify and replace parameters given in the command field. For clarity the `action` field for hooks was renamed to `command`. --- Cargo.lock | 69 +++++++++++++++++++++++++++++ Cargo.toml | 1 + README.md | 22 ++++++---- config.yml | 13 +++++- src/main.rs | 123 +++++++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 205 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66f16dc..2a76573 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,18 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2b_simd" version = "0.5.11" @@ -372,6 +384,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "generic-array" version = "0.14.4" @@ -582,6 +600,19 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lexical-core" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 1.0.0", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.90" @@ -687,6 +718,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check 0.9.3", +] + [[package]] name = "notify" version = "4.0.15" @@ -808,6 +852,12 @@ dependencies = [ "proc-macro2 1.0.24", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rand" version = "0.8.3" @@ -1102,6 +1152,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "subtle" version = "2.4.0" @@ -1130,6 +1186,12 @@ dependencies = [ "unicode-xid 0.2.1", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "termcolor" version = "1.1.2" @@ -1300,6 +1362,7 @@ dependencies = [ "dirs", "env_logger", "log 0.4.14", + "nom", "regex", "rocket", "rocket_contrib", @@ -1381,6 +1444,12 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 1856bae..040f484 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ dirs = "3.0" anyhow = "1.0" log = "0.4" env_logger = "0.8" +nom = "6" diff --git a/README.md b/README.md index d09ee69..34f92ca 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Webhookey -Webhookey basically is a webserver listening for requests as for -example sent as gitea's webhooks. Further, Webhookey allows you to -specifiy rules which are matched against the data received to trigger -certain actions. +Webhookey is a webserver listening for requests as for example sent by +gitea's webhooks. Further, Webhookey allows you to specifiy rules +which are matched against the data received to trigger certain +actions. ## Build @@ -55,12 +55,15 @@ Configuration syntax is YAML and has to be done in following order: Right now there is only the configuration parameter for hooks, here each hook has to be configured, It contains following fields: -- action: optional string for the action to be executed when all - filters match -- secrets: list of secrets -- filters: list of filters +- command: Optional string for a command to be executed when all + filters match. Pointers ([RFC + 6901](https://tools.ietf.org/html/rfc6901)) to JSON fields may be + used to be replaced with data from the JSON data with `{{ + /field/pointed/to }}` +- secrets: List of secrets. +- filters: List of filters. -Each filter has to have following fields: +Each filter must have following fields: - pointer: pointer to the JSON field according to [RFC 6901](https://tools.ietf.org/html/rfc6901) - regex: regular expression which has to match the field pointed to by @@ -88,3 +91,4 @@ Whereas `` depends on the platform: ### Authentication features ### Secure cookies? ## Parameterize fields +## Use proptest or quickcheck for tests of parsers diff --git a/config.yml b/config.yml index a91c33e..5f4cd7f 100644 --- a/config.yml +++ b/config.yml @@ -1,7 +1,7 @@ --- hooks: hook1: - action: /usr/bin/local/script_xy.sh + command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf secrets: - secret_key_01 - secret_key_02 @@ -10,7 +10,16 @@ hooks: pointer: /ref regex: refs/heads/master hook2: - action: /usr/bin/local/script_xyz.sh + command: /usr/bin/local/script_xy.sh asdfasdf + secrets: + - secret_key_01 + - secret_key_02 + filters: + match_ref: + pointer: /ref + regex: refs/heads/master + hook3: + command: /usr/bin/local/script_xyz.sh secrets: - secret_key03 filters: diff --git a/src/main.rs b/src/main.rs index 951ba44..9defde9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,14 @@ use anyhow::{anyhow, bail, Result}; use log::{debug, info, trace, warn}; +use nom::{ + branch::alt, + bytes::complete::{tag, take_until}, + combinator::map_res, + multi::many0, + sequence::delimited, + Finish, IResult, +}; use regex::Regex; use rocket::{fairing::AdHoc, get, http::Status, post, routes, Response, State}; use rocket_contrib::json::Json; @@ -16,7 +24,7 @@ struct Config { #[derive(Debug, Deserialize, Serialize)] struct Hook { - action: Option, + command: Option, secrets: Vec, filters: HashMap, } @@ -35,7 +43,34 @@ fn index() -> &'static str { "Hello, webhookey!" } -fn execute_hook(name: &str, hook: &Hook, data: &serde_json::Value) -> Result<()> { +fn replace_parameter(input: &str, data: &serde_json::Value) -> Result { + let parse: IResult<&str, Vec<&str>> = many0(alt(( + map_res( + delimited(tag("{{"), take_until("}}"), tag("}}")), + |param: &str| { + if let Some(value) = data.pointer(param.trim()) { + if let Some(value) = value.as_str() { + Ok(value) + } else { + bail!("Could not convert field `{}` to string", param.trim()); + } + } else { + bail!("Could not find `{}` in received data", param.trim()); + } + }, + ), + take_until("{{"), + )))(input); + + let (last, mut result) = parse + .finish() + .map_err(|e| anyhow!("Could not parse command: {}", e))?; + result.push(last); + + Ok(result.join("")) +} + +fn execute_hook<'a>(name: &'a str, hook: &'a Hook, data: &'a serde_json::Value) -> Result<()> { debug!("Running hook `{}`", name); for (filter_name, filter) in hook.filters.iter() { @@ -59,25 +94,27 @@ fn execute_hook(name: &str, hook: &Hook, data: &serde_json::Value) -> Result<()> } } - if let Some(action) = &hook.action { - info!("Execute `{}` from hook `{}`", action, name); + if let Some(command) = &hook.command { + let command = replace_parameter(&command, data)?; - let action = action.split(' ').collect::>(); + info!("Execute `{}` from hook `{}`", command, name); - let command = Command::new(action[0]).args(&action[1..]).output()?; + let command = command.split(' ').collect::>(); + + let exec_command = Command::new(&command[0]).args(&command[1..]).output()?; info!( "Command `{}` exited with return code: {}", - action[0], command.status + &command[0], &exec_command.status ); debug!( "Output of command `{}` on stderr: {:?}", - action[0], &command.stderr + &command[0], &exec_command.stderr ); trace!( "Output of command `{}` on stdout: {:?}", - action[0], - &command.stdout + &command[0], + &exec_command.stdout ); } @@ -134,7 +171,7 @@ fn get_config() -> Result { if let Ok(config) = File::open(&path) { info!( "Loading configuration from `{}`", - path.to_str().unwrap_or("path not printable"), + path.to_str().unwrap_or(""), ); return Ok(config); @@ -171,6 +208,7 @@ fn main() -> Result<()> { mod tests { use super::*; use rocket::{http::ContentType, local::Client}; + use serde_json::json; #[test] fn index() { @@ -189,7 +227,7 @@ mod tests { hooks.insert( "test_hook".to_string(), Hook { - action: None, + command: None, secrets: vec!["valid".to_string()], filters: HashMap::new(), }, @@ -239,4 +277,65 @@ mod tests { assert_eq!(response.status(), Status::BadRequest); } + + #[test] + fn parse_command() { + assert_eq!( + replace_parameter("command", &serde_json::Value::Null).unwrap(), + "command" + ); + + assert_eq!( + replace_parameter(" command", &serde_json::Value::Null).unwrap(), + " command" + ); + + assert_eq!( + replace_parameter("command ", &serde_json::Value::Null).unwrap(), + "command " + ); + + assert_eq!( + replace_parameter(" command ", &serde_json::Value::Null).unwrap(), + " command " + ); + + assert_eq!( + replace_parameter("command command ", &serde_json::Value::Null).unwrap(), + "command command " + ); + + assert_eq!( + replace_parameter("{{ /foo }} command", &json!({ "foo": "bar" })).unwrap(), + "bar command" + ); + + assert_eq!( + replace_parameter(" command {{ /foo }} ", &json!({ "foo": "bar" })).unwrap(), + " command bar " + ); + + assert_eq!( + replace_parameter( + "{{ /foo }} command{{/field1/foo}}", + &json!({ "foo": "bar", "field1": { "foo": "baz" } }) + ) + .unwrap(), + "bar commandbaz" + ); + + assert_eq!( + replace_parameter(" command {{ /foo }} ", &json!({ "foo": "bar" })).unwrap(), + " command bar " + ); + + assert_eq!( + replace_parameter( + " {{ /field1/foo }} command", + &json!({ "field1": { "foo": "bar" } }) + ) + .unwrap(), + " bar command" + ); + } }