Use a custom parser for commands

The removes the dependency for `nom` as commands are now parsed with
its own tiny parser.
This commit is contained in:
finga 2021-11-17 13:17:15 +01:00
parent 4b9186b10d
commit 4d39488c32
4 changed files with 89 additions and 109 deletions

20
Cargo.lock generated
View file

@ -738,12 +738,6 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "mio"
version = "0.7.14"
@ -786,17 +780,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "nom"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109"
dependencies = [
"memchr",
"minimal-lexical",
"version_check",
]
[[package]]
name = "ntapi"
version = "0.3.6"
@ -1933,7 +1916,7 @@ dependencies = [
[[package]]
name = "webhookey"
version = "0.1.3"
version = "0.1.5"
dependencies = [
"anyhow",
"clap",
@ -1943,7 +1926,6 @@ dependencies = [
"hmac",
"ipnet",
"log",
"nom",
"regex",
"rocket",
"run_script",

View file

@ -1,6 +1,6 @@
[package]
name = "webhookey"
version = "0.1.4"
version = "0.1.5"
authors = ["finga <webhookey@onders.org>"]
edition = "2021"
license = "GPL-3.0-or-later"
@ -20,7 +20,6 @@ dirs = "4.0"
anyhow = "1.0"
log = "0.4"
env_logger = "0.9"
nom = "7"
hmac = "0.11"
sha2 = "0.9"
hex = "0.4"

View file

@ -2,14 +2,6 @@ use anyhow::{anyhow, bail, Result};
use clap::Parser;
use hmac::{Hmac, Mac, NewMac};
use log::{debug, error, info, trace, warn};
use nom::{
branch::alt,
bytes::complete::{tag, take_until},
combinator::map_res,
multi::many0,
sequence::delimited,
Finish, IResult,
};
use rocket::{
data::{FromData, ToByteUnit},
futures::TryFutureExt,
@ -84,32 +76,71 @@ impl Hook {
request: &Request,
data: &mut serde_json::Value,
) -> Result<String> {
trace!("Replacing parameters for command of hook `{}`", hook_name);
debug!("Replacing parameters for command of hook `{}`", hook_name);
for parameter in self.get_parameter()? {
let parameter = parameter.trim();
Hook::replace_parameters(&self.command, request.headers(), data)
}
if let Some(json_value) = data.pointer(parameter) {
*data.pointer_mut(parameter).ok_or_else(|| {
WebhookeyError::InvalidParameterPointer(parameter.to_string())
})? = serde_json::Value::String(webhooks::get_string(json_value)?);
fn replace_parameters(
input: &str,
headers: &HeaderMap,
data: &serde_json::Value,
) -> Result<String> {
let mut command = String::new();
let command_template = &mut input.chars();
while let Some(i) = command_template.next() {
if i == '{' {
if let Some('{') = command_template.next() {
let mut token = String::new();
while let Some(i) = command_template.next() {
if i == '}' {
if let Some('}') = command_template.next() {
let expr = token.trim().split(' ').collect::<Vec<&str>>();
let replaced = match expr.get(0) {
Some(&"header") => get_header_field(
headers,
expr.get(1).ok_or_else(|| {
anyhow!("Missing parameter for `header` expression")
})?,
)?
.to_string(),
Some(pointer) => webhooks::get_string(
data.pointer(pointer).ok_or_else(|| {
anyhow!(
"Could not find field refered to in parameter `{}`",
pointer
)
})?,
)?,
None => bail!("Missing expression in variable `{}`", token),
};
command.push_str(&replaced);
trace!("Replace `{}` with: {}", token, replaced);
break;
} else {
command.push('}');
command.push(i);
}
} else {
token.push(i);
}
}
} else {
command.push('{');
command.push(i);
}
} else {
command.push(i);
}
}
replace_parameters(&self.command, request.headers(), data)
}
fn get_parameter(&self) -> Result<Vec<&str>> {
let parse: IResult<&str, Vec<&str>> = many0(alt((
delimited(tag("{{"), take_until("}}"), tag("}}")),
take_until("{{"),
)))(&self.command);
let (_last, result) = parse
.finish()
.map_err(|e| anyhow!("Could not get parameters from command: {}", e))?;
Ok(result)
Ok(command)
}
}
@ -214,55 +245,12 @@ fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
mac.verify(&raw_signature).map_err(|e| anyhow!("{}", e))
}
fn get_header_field<'a>(headers: &'a HeaderMap, param: &[&str]) -> Result<&'a str> {
fn get_header_field<'a>(headers: &'a HeaderMap, param: &str) -> Result<&'a str> {
headers
.get_one(
param
.get(1)
.ok_or_else(|| anyhow!("Missing parameter for `header` expression"))?,
)
.get_one(param)
.ok_or_else(|| anyhow!("Could not extract event parameter from header"))
}
fn get_value_from_pointer<'a>(data: &'a serde_json::Value, pointer: &'a str) -> Result<&'a str> {
let value = data
.pointer(pointer)
.ok_or_else(|| anyhow!("Could not get field from pointer {}", pointer))?;
value
.as_str()
.ok_or_else(|| anyhow!("Could not convert value `{}` to string", value))
}
fn replace_parameters(
input: &str,
headers: &HeaderMap,
data: &serde_json::Value,
) -> Result<String> {
let parse: IResult<&str, Vec<&str>> = many0(alt((
map_res(
delimited(tag("{{"), take_until("}}"), tag("}}")),
|param: &str| {
let expr = param.trim().split(' ').collect::<Vec<&str>>();
match expr.get(0) {
Some(&"header") => get_header_field(headers, &expr),
Some(pointer) => get_value_from_pointer(data, pointer),
None => bail!("Missing expression in `{}`", input),
}
},
),
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 get_config() -> Result<File> {
// Look for config in CWD..
if let Ok(config) = File::open("config.yml") {
@ -583,37 +571,40 @@ mod tests {
headers.add_raw("X-Gitea-Event", "something");
assert_eq!(
replace_parameters("command", &headers, &serde_json::Value::Null).unwrap(),
Hook::replace_parameters("command", &headers, &serde_json::Value::Null).unwrap(),
"command"
);
assert_eq!(
replace_parameters(" command", &headers, &serde_json::Value::Null).unwrap(),
Hook::replace_parameters(" command", &headers, &serde_json::Value::Null).unwrap(),
" command"
);
assert_eq!(
replace_parameters("command ", &headers, &serde_json::Value::Null).unwrap(),
Hook::replace_parameters("command ", &headers, &serde_json::Value::Null).unwrap(),
"command "
);
assert_eq!(
replace_parameters(" command ", &headers, &serde_json::Value::Null).unwrap(),
Hook::replace_parameters(" command ", &headers, &serde_json::Value::Null)
.unwrap(),
" command "
);
assert_eq!(
replace_parameters("command command ", &headers, &serde_json::Value::Null).unwrap(),
Hook::replace_parameters("command command ", &headers, &serde_json::Value::Null)
.unwrap(),
"command command "
);
assert_eq!(
replace_parameters("{{ /foo }} command", &headers, &json!({ "foo": "bar" })).unwrap(),
Hook::replace_parameters("{{ /foo }} command", &headers, &json!({ "foo": "bar" }))
.unwrap(),
"bar command"
);
assert_eq!(
replace_parameters(
Hook::replace_parameters(
" command {{ /foo }} ",
&headers,
&json!({ "foo": "bar" })
@ -623,7 +614,7 @@ mod tests {
);
assert_eq!(
replace_parameters(
Hook::replace_parameters(
"{{ /foo }} command{{/field1/foo}}",
&headers,
&json!({ "foo": "bar", "field1": { "foo": "baz" } })
@ -633,7 +624,7 @@ mod tests {
);
assert_eq!(
replace_parameters(
Hook::replace_parameters(
" command {{ /foo }} ",
&headers,
&json!({ "foo": "bar" })
@ -643,7 +634,7 @@ mod tests {
);
assert_eq!(
replace_parameters(
Hook::replace_parameters(
" {{ /field1/foo }} command",
&headers,
&json!({ "field1": { "foo": "bar" } })
@ -653,7 +644,7 @@ mod tests {
);
assert_eq!(
replace_parameters(
Hook::replace_parameters(
" {{ header X-Gitea-Event }} command",
&headers,
&json!({ "field1": { "foo": "bar" } })
@ -663,7 +654,7 @@ mod tests {
);
assert_eq!(
replace_parameters(
Hook::replace_parameters(
" {{ header X-Gitea-Event }} {{ /field1/foo }} command",
&headers,
&json!({ "field1": { "foo": "bar" } })
@ -671,6 +662,16 @@ mod tests {
.unwrap(),
" something bar command"
);
assert_eq!(
Hook::replace_parameters(
" {{ header X-Gitea-Event }} {{ /field1/foo }} {{ /field1/bar }} {{ /field2/foo }} --command{{ /cmd }}",
&headers,
&json!({ "field1": { "foo": "bar", "bar": "baz" }, "field2": { "foo": "qux" }, "cmd": " else"})
)
.unwrap(),
" something bar baz qux --command else"
);
}
#[rocket::async_test]

View file

@ -14,8 +14,6 @@ pub enum WebhookeyError {
Unauthorized(IpAddr),
#[error("Unmatched hook from `{0}`")]
UnmatchedHook(IpAddr),
#[error("Could not find field refered to in parameter `{0}`")]
InvalidParameterPointer(String),
#[error("Could not evaluate filter request")]
InvalidFilter,
#[error("IO Error")]