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"
|
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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
22
README.md
22
README.md
|
@ -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
|
||||||
|
|
13
config.yml
13
config.yml
|
@ -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:
|
||||||
|
|
123
src/main.rs
123
src/main.rs
|
@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue