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:
parent
12c3b12c31
commit
0610fd49c9
5 changed files with 205 additions and 23 deletions
69
Cargo.lock
generated
69
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -18,3 +18,4 @@ dirs = "3.0"
|
|||
anyhow = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.8"
|
||||
nom = "6"
|
||||
|
|
22
README.md
22
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 `<config_dir>` depends on the platform:
|
|||
### Authentication features
|
||||
### Secure cookies?
|
||||
## Parameterize fields
|
||||
## Use proptest or quickcheck for tests of parsers
|
||||
|
|
13
config.yml
13
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:
|
||||
|
|
123
src/main.rs
123
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<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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue