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`.
This commit is contained in:
finga 2021-03-21 15:51:58 +01:00
parent 12c3b12c31
commit 0610fd49c9
5 changed files with 205 additions and 23 deletions

69
Cargo.lock generated
View file

@ -128,6 +128,18 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 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]] [[package]]
name = "blake2b_simd" name = "blake2b_simd"
version = "0.5.11" version = "0.5.11"
@ -372,6 +384,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.4" version = "0.14.4"
@ -582,6 +600,19 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 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]] [[package]]
name = "libc" name = "libc"
version = "0.2.90" version = "0.2.90"
@ -687,6 +718,19 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "notify" name = "notify"
version = "4.0.15" version = "4.0.15"
@ -808,6 +852,12 @@ dependencies = [
"proc-macro2 1.0.24", "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]] [[package]]
name = "rand" name = "rand"
version = "0.8.3" version = "0.8.3"
@ -1102,6 +1152,12 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483" checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.4.0" version = "2.4.0"
@ -1130,6 +1186,12 @@ dependencies = [
"unicode-xid 0.2.1", "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]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.1.2" version = "1.1.2"
@ -1300,6 +1362,7 @@ dependencies = [
"dirs", "dirs",
"env_logger", "env_logger",
"log 0.4.14", "log 0.4.14",
"nom",
"regex", "regex",
"rocket", "rocket",
"rocket_contrib", "rocket_contrib",
@ -1381,6 +1444,12 @@ dependencies = [
"winapi-build", "winapi-build",
] ]
[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]] [[package]]
name = "yaml-rust" name = "yaml-rust"
version = "0.4.5" version = "0.4.5"

View file

@ -18,3 +18,4 @@ dirs = "3.0"
anyhow = "1.0" anyhow = "1.0"
log = "0.4" log = "0.4"
env_logger = "0.8" env_logger = "0.8"
nom = "6"

View file

@ -1,8 +1,8 @@
# Webhookey # Webhookey
Webhookey basically is a webserver listening for requests as for Webhookey is a webserver listening for requests as for example sent by
example sent as gitea's webhooks. Further, Webhookey allows you to gitea's webhooks. Further, Webhookey allows you to specifiy rules
specifiy rules which are matched against the data received to trigger which are matched against the data received to trigger certain
certain actions. actions.
## Build ## 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 Right now there is only the configuration parameter for hooks, here
each hook has to be configured, It contains following fields: each hook has to be configured, It contains following fields:
- action: optional string for the action to be executed when all - command: Optional string for a command to be executed when all
filters match filters match. Pointers ([RFC
- secrets: list of secrets 6901](https://tools.ietf.org/html/rfc6901)) to JSON fields may be
- filters: list of filters 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 - pointer: pointer to the JSON field according to [RFC
6901](https://tools.ietf.org/html/rfc6901) 6901](https://tools.ietf.org/html/rfc6901)
- regex: regular expression which has to match the field pointed to by - regex: regular expression which has to match the field pointed to by
@ -88,3 +91,4 @@ Whereas `<config_dir>` depends on the platform:
### Authentication features ### Authentication features
### Secure cookies? ### Secure cookies?
## Parameterize fields ## Parameterize fields
## Use proptest or quickcheck for tests of parsers

View file

@ -1,7 +1,7 @@
--- ---
hooks: hooks:
hook1: hook1:
action: /usr/bin/local/script_xy.sh command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf
secrets: secrets:
- secret_key_01 - secret_key_01
- secret_key_02 - secret_key_02
@ -10,7 +10,16 @@ hooks:
pointer: /ref pointer: /ref
regex: refs/heads/master regex: refs/heads/master
hook2: 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: secrets:
- secret_key03 - secret_key03
filters: filters:

View file

@ -2,6 +2,14 @@
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use log::{debug, info, trace, warn}; 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 regex::Regex;
use rocket::{fairing::AdHoc, get, http::Status, post, routes, Response, State}; use rocket::{fairing::AdHoc, get, http::Status, post, routes, Response, State};
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
@ -16,7 +24,7 @@ struct Config {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
struct Hook { struct Hook {
action: Option<String>, command: Option<String>,
secrets: Vec<String>, secrets: Vec<String>,
filters: HashMap<String, Filter>, filters: HashMap<String, Filter>,
} }
@ -35,7 +43,34 @@ fn index() -> &'static str {
"Hello, webhookey!" "Hello, webhookey!"
} }
fn execute_hook(name: &str, hook: &Hook, data: &serde_json::Value) -> Result<()> { fn replace_parameter(input: &str, data: &serde_json::Value) -> Result<String> {
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); debug!("Running hook `{}`", name);
for (filter_name, filter) in hook.filters.iter() { 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 { if let Some(command) = &hook.command {
info!("Execute `{}` from hook `{}`", action, name); let command = replace_parameter(&command, data)?;
let action = action.split(' ').collect::<Vec<&str>>(); info!("Execute `{}` from hook `{}`", command, name);
let command = Command::new(action[0]).args(&action[1..]).output()?; let command = command.split(' ').collect::<Vec<&str>>();
let exec_command = Command::new(&command[0]).args(&command[1..]).output()?;
info!( info!(
"Command `{}` exited with return code: {}", "Command `{}` exited with return code: {}",
action[0], command.status &command[0], &exec_command.status
); );
debug!( debug!(
"Output of command `{}` on stderr: {:?}", "Output of command `{}` on stderr: {:?}",
action[0], &command.stderr &command[0], &exec_command.stderr
); );
trace!( trace!(
"Output of command `{}` on stdout: {:?}", "Output of command `{}` on stdout: {:?}",
action[0], &command[0],
&command.stdout &exec_command.stdout
); );
} }
@ -134,7 +171,7 @@ fn get_config() -> Result<File> {
if let Ok(config) = File::open(&path) { if let Ok(config) = File::open(&path) {
info!( info!(
"Loading configuration from `{}`", "Loading configuration from `{}`",
path.to_str().unwrap_or("path not printable"), path.to_str().unwrap_or("<path unprintable>"),
); );
return Ok(config); return Ok(config);
@ -171,6 +208,7 @@ fn main() -> Result<()> {
mod tests { mod tests {
use super::*; use super::*;
use rocket::{http::ContentType, local::Client}; use rocket::{http::ContentType, local::Client};
use serde_json::json;
#[test] #[test]
fn index() { fn index() {
@ -189,7 +227,7 @@ mod tests {
hooks.insert( hooks.insert(
"test_hook".to_string(), "test_hook".to_string(),
Hook { Hook {
action: None, command: None,
secrets: vec!["valid".to_string()], secrets: vec!["valid".to_string()],
filters: HashMap::new(), filters: HashMap::new(),
}, },
@ -239,4 +277,65 @@ mod tests {
assert_eq!(response.status(), Status::BadRequest); 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"
);
}
} }