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:
parent
4b9186b10d
commit
4d39488c32
4 changed files with 89 additions and 109 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -738,12 +738,6 @@ version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimal-lexical"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.7.14"
|
version = "0.7.14"
|
||||||
|
@ -786,17 +780,6 @@ dependencies = [
|
||||||
"version_check",
|
"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]]
|
[[package]]
|
||||||
name = "ntapi"
|
name = "ntapi"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
@ -1933,7 +1916,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webhookey"
|
name = "webhookey"
|
||||||
version = "0.1.3"
|
version = "0.1.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
@ -1943,7 +1926,6 @@ dependencies = [
|
||||||
"hmac",
|
"hmac",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"log",
|
"log",
|
||||||
"nom",
|
|
||||||
"regex",
|
"regex",
|
||||||
"rocket",
|
"rocket",
|
||||||
"run_script",
|
"run_script",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "webhookey"
|
name = "webhookey"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
authors = ["finga <webhookey@onders.org>"]
|
authors = ["finga <webhookey@onders.org>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
@ -20,7 +20,6 @@ dirs = "4.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
nom = "7"
|
|
||||||
hmac = "0.11"
|
hmac = "0.11"
|
||||||
sha2 = "0.9"
|
sha2 = "0.9"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
|
173
src/main.rs
173
src/main.rs
|
@ -2,14 +2,6 @@ use anyhow::{anyhow, bail, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use hmac::{Hmac, Mac, NewMac};
|
use hmac::{Hmac, Mac, NewMac};
|
||||||
use log::{debug, error, info, trace, warn};
|
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::{
|
use rocket::{
|
||||||
data::{FromData, ToByteUnit},
|
data::{FromData, ToByteUnit},
|
||||||
futures::TryFutureExt,
|
futures::TryFutureExt,
|
||||||
|
@ -84,32 +76,71 @@ impl Hook {
|
||||||
request: &Request,
|
request: &Request,
|
||||||
data: &mut serde_json::Value,
|
data: &mut serde_json::Value,
|
||||||
) -> Result<String> {
|
) -> 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()? {
|
Hook::replace_parameters(&self.command, request.headers(), data)
|
||||||
let parameter = parameter.trim();
|
}
|
||||||
|
|
||||||
if let Some(json_value) = data.pointer(parameter) {
|
fn replace_parameters(
|
||||||
*data.pointer_mut(parameter).ok_or_else(|| {
|
input: &str,
|
||||||
WebhookeyError::InvalidParameterPointer(parameter.to_string())
|
headers: &HeaderMap,
|
||||||
})? = serde_json::Value::String(webhooks::get_string(json_value)?);
|
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)
|
Ok(command)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,55 +245,12 @@ fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
|
||||||
mac.verify(&raw_signature).map_err(|e| anyhow!("{}", e))
|
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
|
headers
|
||||||
.get_one(
|
.get_one(param)
|
||||||
param
|
|
||||||
.get(1)
|
|
||||||
.ok_or_else(|| anyhow!("Missing parameter for `header` expression"))?,
|
|
||||||
)
|
|
||||||
.ok_or_else(|| anyhow!("Could not extract event parameter from header"))
|
.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> {
|
fn get_config() -> Result<File> {
|
||||||
// Look for config in CWD..
|
// Look for config in CWD..
|
||||||
if let Ok(config) = File::open("config.yml") {
|
if let Ok(config) = File::open("config.yml") {
|
||||||
|
@ -583,37 +571,40 @@ mod tests {
|
||||||
headers.add_raw("X-Gitea-Event", "something");
|
headers.add_raw("X-Gitea-Event", "something");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters("command", &headers, &serde_json::Value::Null).unwrap(),
|
Hook::replace_parameters("command", &headers, &serde_json::Value::Null).unwrap(),
|
||||||
"command"
|
"command"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters(" command", &headers, &serde_json::Value::Null).unwrap(),
|
Hook::replace_parameters(" command", &headers, &serde_json::Value::Null).unwrap(),
|
||||||
" command"
|
" command"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters("command ", &headers, &serde_json::Value::Null).unwrap(),
|
Hook::replace_parameters("command ", &headers, &serde_json::Value::Null).unwrap(),
|
||||||
"command "
|
"command "
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters(" command ", &headers, &serde_json::Value::Null).unwrap(),
|
Hook::replace_parameters(" command ", &headers, &serde_json::Value::Null)
|
||||||
|
.unwrap(),
|
||||||
" command "
|
" command "
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
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 "
|
"command command "
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters("{{ /foo }} command", &headers, &json!({ "foo": "bar" })).unwrap(),
|
Hook::replace_parameters("{{ /foo }} command", &headers, &json!({ "foo": "bar" }))
|
||||||
|
.unwrap(),
|
||||||
"bar command"
|
"bar command"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters(
|
Hook::replace_parameters(
|
||||||
" command {{ /foo }} ",
|
" command {{ /foo }} ",
|
||||||
&headers,
|
&headers,
|
||||||
&json!({ "foo": "bar" })
|
&json!({ "foo": "bar" })
|
||||||
|
@ -623,7 +614,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters(
|
Hook::replace_parameters(
|
||||||
"{{ /foo }} command{{/field1/foo}}",
|
"{{ /foo }} command{{/field1/foo}}",
|
||||||
&headers,
|
&headers,
|
||||||
&json!({ "foo": "bar", "field1": { "foo": "baz" } })
|
&json!({ "foo": "bar", "field1": { "foo": "baz" } })
|
||||||
|
@ -633,7 +624,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters(
|
Hook::replace_parameters(
|
||||||
" command {{ /foo }} ",
|
" command {{ /foo }} ",
|
||||||
&headers,
|
&headers,
|
||||||
&json!({ "foo": "bar" })
|
&json!({ "foo": "bar" })
|
||||||
|
@ -643,7 +634,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters(
|
Hook::replace_parameters(
|
||||||
" {{ /field1/foo }} command",
|
" {{ /field1/foo }} command",
|
||||||
&headers,
|
&headers,
|
||||||
&json!({ "field1": { "foo": "bar" } })
|
&json!({ "field1": { "foo": "bar" } })
|
||||||
|
@ -653,7 +644,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters(
|
Hook::replace_parameters(
|
||||||
" {{ header X-Gitea-Event }} command",
|
" {{ header X-Gitea-Event }} command",
|
||||||
&headers,
|
&headers,
|
||||||
&json!({ "field1": { "foo": "bar" } })
|
&json!({ "field1": { "foo": "bar" } })
|
||||||
|
@ -663,7 +654,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_parameters(
|
Hook::replace_parameters(
|
||||||
" {{ header X-Gitea-Event }} {{ /field1/foo }} command",
|
" {{ header X-Gitea-Event }} {{ /field1/foo }} command",
|
||||||
&headers,
|
&headers,
|
||||||
&json!({ "field1": { "foo": "bar" } })
|
&json!({ "field1": { "foo": "bar" } })
|
||||||
|
@ -671,6 +662,16 @@ mod tests {
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
" something bar command"
|
" 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]
|
#[rocket::async_test]
|
||||||
|
|
|
@ -14,8 +14,6 @@ pub enum WebhookeyError {
|
||||||
Unauthorized(IpAddr),
|
Unauthorized(IpAddr),
|
||||||
#[error("Unmatched hook from `{0}`")]
|
#[error("Unmatched hook from `{0}`")]
|
||||||
UnmatchedHook(IpAddr),
|
UnmatchedHook(IpAddr),
|
||||||
#[error("Could not find field refered to in parameter `{0}`")]
|
|
||||||
InvalidParameterPointer(String),
|
|
||||||
#[error("Could not evaluate filter request")]
|
#[error("Could not evaluate filter request")]
|
||||||
InvalidFilter,
|
InvalidFilter,
|
||||||
#[error("IO Error")]
|
#[error("IO Error")]
|
||||||
|
|
Loading…
Reference in a new issue