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"
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"

View file

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

View file

@ -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 `<config_dir>` depends on the platform:
### Authentication features
### Secure cookies?
## Parameterize fields
## Use proptest or quickcheck for tests of parsers

View file

@ -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:

View file

@ -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<String>,
command: Option<String>,
secrets: Vec<String>,
filters: HashMap<String, Filter>,
}
@ -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<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);
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::<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!(
"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<File> {
if let Ok(config) = File::open(&path) {
info!(
"Loading configuration from `{}`",
path.to_str().unwrap_or("path not printable"),
path.to_str().unwrap_or("<path unprintable>"),
);
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"
);
}
}