2021-02-02 11:05:50 +01:00
|
|
|
#![feature(proc_macro_hygiene, decl_macro)]
|
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
use anyhow::{anyhow, bail, Result};
|
2021-03-28 03:50:52 +02:00
|
|
|
use hmac::{Hmac, Mac, NewMac};
|
|
|
|
use log::{debug, error, info, trace, warn};
|
2021-03-21 15:51:58 +01:00
|
|
|
use nom::{
|
|
|
|
branch::alt,
|
|
|
|
bytes::complete::{tag, take_until},
|
|
|
|
combinator::map_res,
|
|
|
|
multi::many0,
|
|
|
|
sequence::delimited,
|
|
|
|
Finish, IResult,
|
|
|
|
};
|
2021-03-03 15:24:46 +01:00
|
|
|
use regex::Regex;
|
2021-03-28 03:50:52 +02:00
|
|
|
use rocket::{
|
|
|
|
data::{self, FromDataSimple},
|
|
|
|
fairing::AdHoc,
|
|
|
|
get,
|
|
|
|
http::{HeaderMap, Status},
|
|
|
|
post, routes, Data,
|
|
|
|
Outcome::{Failure, Success},
|
|
|
|
Request, Response, State,
|
|
|
|
};
|
2021-02-02 11:05:50 +01:00
|
|
|
use serde::{Deserialize, Serialize};
|
2021-03-28 03:50:52 +02:00
|
|
|
use sha2::Sha256;
|
2021-02-02 11:05:50 +01:00
|
|
|
|
2021-03-22 11:12:45 +01:00
|
|
|
use std::{
|
2021-03-28 03:50:52 +02:00
|
|
|
collections::HashMap,
|
|
|
|
fs::File,
|
|
|
|
io::{BufReader, Read},
|
|
|
|
net::SocketAddr,
|
|
|
|
process::Command,
|
2021-03-22 11:12:45 +01:00
|
|
|
str::from_utf8,
|
|
|
|
};
|
2021-03-03 15:24:46 +01:00
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
|
|
struct Config {
|
|
|
|
hooks: HashMap<String, Hook>,
|
|
|
|
}
|
|
|
|
|
2021-02-02 11:05:50 +01:00
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
2021-03-03 15:24:46 +01:00
|
|
|
struct Hook {
|
2021-03-28 03:50:52 +02:00
|
|
|
command: String,
|
2021-03-29 04:21:31 +02:00
|
|
|
signature: String,
|
2021-03-19 10:16:46 +01:00
|
|
|
secrets: Vec<String>,
|
2021-03-03 15:24:46 +01:00
|
|
|
filters: HashMap<String, Filter>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
|
|
struct Filter {
|
|
|
|
pointer: String,
|
|
|
|
regex: String,
|
|
|
|
}
|
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
#[derive(Debug)]
|
2021-03-29 04:21:31 +02:00
|
|
|
struct Hooks(HashMap<String, String>);
|
2021-02-02 11:05:50 +01:00
|
|
|
|
2021-03-29 02:19:30 +02:00
|
|
|
fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
|
|
|
|
let mut mac = Hmac::<Sha256>::new_varkey(&secret.as_bytes())
|
|
|
|
.map_err(|e| anyhow!("Could not create hasher with secret: {}", e))?;
|
|
|
|
mac.update(&data);
|
|
|
|
let raw_signature = hex::decode(signature.as_bytes())?;
|
|
|
|
mac.verify(&raw_signature).map_err(|e| anyhow!("{}", e))
|
|
|
|
}
|
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
fn replace_parameter(input: &str, headers: &HeaderMap, data: &serde_json::Value) -> Result<String> {
|
2021-03-21 15:51:58 +01:00
|
|
|
let parse: IResult<&str, Vec<&str>> = many0(alt((
|
|
|
|
map_res(
|
|
|
|
delimited(tag("{{"), take_until("}}"), tag("}}")),
|
2021-03-28 03:50:52 +02:00
|
|
|
|param: &str| match param.trim() {
|
|
|
|
"event" => {
|
|
|
|
if let Some(event) = headers.get_one("X-Gitea-Event") {
|
|
|
|
Ok(event)
|
2021-03-21 15:51:58 +01:00
|
|
|
} else {
|
2021-03-28 03:50:52 +02:00
|
|
|
bail!("Could not extract event parameter from header");
|
2021-03-21 15:51:58 +01:00
|
|
|
}
|
|
|
|
}
|
2021-03-28 03:50:52 +02:00
|
|
|
pointer => match data.pointer(pointer) {
|
|
|
|
Some(value) => match value.as_str() {
|
|
|
|
Some(value) => Ok(value),
|
|
|
|
_ => bail!("Could not convert value `{}` to string", value),
|
|
|
|
},
|
|
|
|
_ => bail!("Could not convert field `{}` to string", param.trim()),
|
|
|
|
},
|
2021-03-21 15:51:58 +01:00
|
|
|
},
|
|
|
|
),
|
|
|
|
take_until("{{"),
|
|
|
|
)))(input);
|
|
|
|
|
|
|
|
let (last, mut result) = parse
|
|
|
|
.finish()
|
|
|
|
.map_err(|e| anyhow!("Could not parse command: {}", e))?;
|
|
|
|
result.push(last);
|
|
|
|
|
|
|
|
Ok(result.join(""))
|
|
|
|
}
|
|
|
|
|
2021-03-29 02:19:30 +02:00
|
|
|
fn filter_match(
|
|
|
|
hook_name: &str,
|
|
|
|
hook: &Hook,
|
|
|
|
filter_name: &str,
|
|
|
|
filter: &Filter,
|
|
|
|
request: &Request,
|
|
|
|
data: &serde_json::Value,
|
|
|
|
) -> Result<Option<String>> {
|
|
|
|
trace!("Matching filter `{}` of hook `{}`", filter_name, hook_name);
|
|
|
|
|
|
|
|
let regex = Regex::new(&filter.regex)?;
|
|
|
|
|
|
|
|
if let Some(value) = data.pointer(&filter.pointer) {
|
|
|
|
if let Some(value) = value.as_str() {
|
|
|
|
if regex.is_match(value) {
|
|
|
|
debug!("Filter `{}` of hook `{}` matched", filter_name, hook_name);
|
|
|
|
|
|
|
|
return Ok(Some(replace_parameter(
|
|
|
|
&hook.command.to_string(),
|
|
|
|
&request.headers(),
|
|
|
|
data,
|
|
|
|
)?));
|
|
|
|
}
|
|
|
|
} else {
|
2021-03-29 04:21:31 +02:00
|
|
|
bail!(
|
2021-03-29 02:19:30 +02:00
|
|
|
"Could not parse pointer in hook `{}` from filter `{}`",
|
|
|
|
hook_name,
|
|
|
|
filter_name
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
trace!(
|
|
|
|
"Filter `{}` of hook `{}` did not match",
|
|
|
|
filter_name,
|
|
|
|
hook_name
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
impl FromDataSimple for Hooks {
|
|
|
|
type Error = anyhow::Error;
|
2021-03-03 15:24:46 +01:00
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
|
2021-03-29 04:21:31 +02:00
|
|
|
let mut buffer = Vec::new();
|
|
|
|
match data.open().read_to_end(&mut buffer) {
|
|
|
|
Ok(size) => info!("Data of size {} received", size),
|
|
|
|
Err(e) => {
|
|
|
|
error!("Could not read to end of data: {}", &e);
|
|
|
|
return Failure((
|
|
|
|
Status::BadRequest,
|
|
|
|
anyhow!("Could not read to end of data: {}", &e),
|
|
|
|
));
|
2021-03-03 15:24:46 +01:00
|
|
|
}
|
2021-03-29 04:21:31 +02:00
|
|
|
}
|
2021-03-03 15:24:46 +01:00
|
|
|
|
2021-03-29 04:21:31 +02:00
|
|
|
let config = request.guard::<State<Config>>().unwrap(); // should never fail
|
|
|
|
let mut valid = false;
|
|
|
|
let mut hooks = HashMap::new();
|
2021-03-28 03:50:52 +02:00
|
|
|
|
2021-03-29 04:21:31 +02:00
|
|
|
for (hook_name, hook) in &config.hooks {
|
|
|
|
if let Some(signature) = request.headers().get_one(&hook.signature) {
|
2021-03-28 03:50:52 +02:00
|
|
|
for secret in &hook.secrets {
|
2021-03-29 02:19:30 +02:00
|
|
|
match validate_request(&secret, &signature, &buffer) {
|
|
|
|
Ok(()) => {
|
2021-03-29 04:21:31 +02:00
|
|
|
trace!("Valid signature found for hook `{}`", hook_name,);
|
2021-03-29 02:19:30 +02:00
|
|
|
|
|
|
|
valid = true;
|
|
|
|
|
|
|
|
let data: serde_json::Value = match serde_json::from_slice(&buffer) {
|
|
|
|
Ok(data) => data,
|
|
|
|
Err(e) => {
|
|
|
|
error!("Could not parse json: {}", e);
|
|
|
|
return Failure((
|
|
|
|
Status::BadRequest,
|
|
|
|
anyhow!("Could not parse json: {}", e),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
for (filter_name, filter) in &hook.filters {
|
|
|
|
match filter_match(
|
|
|
|
&hook_name,
|
|
|
|
&hook,
|
|
|
|
&filter_name,
|
|
|
|
&filter,
|
|
|
|
&request,
|
|
|
|
&data,
|
|
|
|
) {
|
2021-03-29 04:21:31 +02:00
|
|
|
Ok(Some(command)) => {
|
|
|
|
hooks.insert(hook_name.to_string(), command);
|
|
|
|
break;
|
|
|
|
}
|
2021-03-29 02:19:30 +02:00
|
|
|
Ok(None) => {}
|
|
|
|
Err(e) => error!("{}", e),
|
2021-03-28 03:50:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(e) => {
|
2021-03-29 02:19:30 +02:00
|
|
|
error!("Could not validate request: {}", e);
|
2021-03-28 03:50:52 +02:00
|
|
|
return Failure((
|
2021-03-29 02:19:30 +02:00
|
|
|
Status::Unauthorized,
|
|
|
|
anyhow!("Could not validate request: {}", e),
|
2021-03-28 03:50:52 +02:00
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-29 04:21:31 +02:00
|
|
|
} else {
|
|
|
|
error!("Could not extract signature from header");
|
|
|
|
return Failure((
|
|
|
|
Status::BadRequest,
|
|
|
|
anyhow!("Could not extract signature from header"),
|
|
|
|
));
|
2021-03-28 03:50:52 +02:00
|
|
|
}
|
2021-03-29 04:21:31 +02:00
|
|
|
}
|
2021-03-17 13:40:08 +01:00
|
|
|
|
2021-03-29 04:21:31 +02:00
|
|
|
if hooks.is_empty() {
|
|
|
|
if valid {
|
|
|
|
warn!("Unmatched hook from {:?}", &request.client_ip());
|
|
|
|
return Failure((
|
|
|
|
Status::NotFound,
|
|
|
|
anyhow!("Unmatched hook from {:?}", &request.client_ip()),
|
|
|
|
));
|
2021-03-28 03:50:52 +02:00
|
|
|
} else {
|
2021-03-29 04:21:31 +02:00
|
|
|
error!("Unauthorized request from {:?}", &request.client_ip());
|
|
|
|
return Failure((
|
|
|
|
Status::Unauthorized,
|
|
|
|
anyhow!("Unauthorized request from {:?}", &request.client_ip()),
|
|
|
|
));
|
2021-03-28 03:50:52 +02:00
|
|
|
}
|
|
|
|
}
|
2021-03-29 04:21:31 +02:00
|
|
|
|
|
|
|
Success(Hooks(hooks))
|
2021-03-03 15:24:46 +01:00
|
|
|
}
|
2021-03-28 03:50:52 +02:00
|
|
|
}
|
2021-03-03 15:24:46 +01:00
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
#[get("/")]
|
|
|
|
fn index() -> &'static str {
|
|
|
|
"Hello, webhookey!"
|
2021-03-03 15:24:46 +01:00
|
|
|
}
|
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
#[post("/", format = "json", data = "<hooks>")]
|
|
|
|
fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> {
|
2021-03-20 00:12:01 +01:00
|
|
|
info!("Post request received from: {}", address);
|
2021-03-03 16:14:54 +01:00
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
for hook in hooks.0 {
|
2021-03-29 04:21:31 +02:00
|
|
|
info!("Execute `{}` from hook `{}`", &hook.1, &hook.0);
|
|
|
|
|
|
|
|
let command = hook.1.split(' ').collect::<Vec<&str>>();
|
|
|
|
match Command::new(&command[0]).args(&command[1..]).output() {
|
|
|
|
Ok(executed) => {
|
|
|
|
info!(
|
|
|
|
"Command `{}` exited with return code: {}",
|
|
|
|
&command[0], &executed.status
|
|
|
|
);
|
|
|
|
trace!(
|
|
|
|
"Output of command `{}` on stdout: {:?}",
|
|
|
|
&command[0],
|
|
|
|
from_utf8(&executed.stdout)?
|
|
|
|
);
|
|
|
|
debug!(
|
|
|
|
"Output of command `{}` on stderr: {:?}",
|
|
|
|
&command[0],
|
|
|
|
from_utf8(&executed.stderr)?
|
|
|
|
);
|
|
|
|
}
|
|
|
|
Err(e) => {
|
|
|
|
error!("Execution of `{}` failed: {}", command[0], e);
|
2021-03-19 10:16:46 +01:00
|
|
|
}
|
|
|
|
}
|
2021-03-03 15:24:46 +01:00
|
|
|
}
|
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
Ok(Response::new())
|
2021-02-02 11:05:50 +01:00
|
|
|
}
|
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
fn get_config() -> Result<File> {
|
|
|
|
if let Ok(config) = File::open("/etc/webhookey/config.yml") {
|
2021-03-03 16:14:54 +01:00
|
|
|
info!("Loading configuration from `/etc/webhookey/config.yml`");
|
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
return Ok(config);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(mut path) = dirs::config_dir() {
|
|
|
|
path.push("webhookey/config.yml");
|
|
|
|
|
2021-03-03 16:14:54 +01:00
|
|
|
if let Ok(config) = File::open(&path) {
|
|
|
|
info!(
|
|
|
|
"Loading configuration from `{}`",
|
2021-03-21 15:51:58 +01:00
|
|
|
path.to_str().unwrap_or("<path unprintable>"),
|
2021-03-03 16:14:54 +01:00
|
|
|
);
|
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
return Ok(config);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Ok(config) = File::open("config.yml") {
|
2021-03-03 16:14:54 +01:00
|
|
|
info!("Loading configuration from `./config.yml`");
|
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
return Ok(config);
|
|
|
|
}
|
|
|
|
|
|
|
|
bail!("No configuration files found.");
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() -> Result<()> {
|
2021-03-03 16:14:54 +01:00
|
|
|
env_logger::init();
|
|
|
|
|
2021-03-20 00:12:01 +01:00
|
|
|
let config: Config = serde_yaml::from_reader(BufReader::new(get_config()?))?;
|
2021-03-03 15:24:46 +01:00
|
|
|
|
2021-03-03 16:14:54 +01:00
|
|
|
trace!("Parsed configuration:\n{}", serde_yaml::to_string(&config)?);
|
2021-03-03 15:24:46 +01:00
|
|
|
|
2021-02-02 11:05:50 +01:00
|
|
|
rocket::ignite()
|
|
|
|
.mount("/", routes![index, receive_hook])
|
2021-03-03 15:24:46 +01:00
|
|
|
.attach(AdHoc::on_attach("webhookey config", move |rocket| {
|
|
|
|
Ok(rocket.manage(config))
|
|
|
|
}))
|
2021-02-02 11:05:50 +01:00
|
|
|
.launch();
|
2021-03-03 15:24:46 +01:00
|
|
|
|
|
|
|
Ok(())
|
2021-02-02 11:05:50 +01:00
|
|
|
}
|
2021-03-20 00:12:01 +01:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2021-03-28 03:50:52 +02:00
|
|
|
use rocket::{
|
|
|
|
http::{ContentType, Header},
|
|
|
|
local::Client,
|
|
|
|
};
|
2021-03-21 15:51:58 +01:00
|
|
|
use serde_json::json;
|
2021-03-20 00:12:01 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn index() {
|
|
|
|
let rocket = rocket::ignite().mount("/", routes![index]);
|
|
|
|
|
|
|
|
let client = Client::new(rocket).unwrap();
|
|
|
|
let mut response = client.get("/").dispatch();
|
|
|
|
|
|
|
|
assert_eq!(response.status(), Status::Ok);
|
|
|
|
assert_eq!(response.body_string(), Some("Hello, webhookey!".into()));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn secret() {
|
|
|
|
let mut hooks = HashMap::new();
|
|
|
|
hooks.insert(
|
|
|
|
"test_hook".to_string(),
|
|
|
|
Hook {
|
2021-03-28 03:50:52 +02:00
|
|
|
command: "".to_string(),
|
2021-03-29 04:21:31 +02:00
|
|
|
signature: "X-Gitea-Signature".to_string(),
|
2021-03-20 00:12:01 +01:00
|
|
|
secrets: vec!["valid".to_string()],
|
|
|
|
filters: HashMap::new(),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
let config = Config { hooks: hooks };
|
|
|
|
|
|
|
|
let rocket = rocket::ignite()
|
|
|
|
.mount("/", routes![receive_hook])
|
|
|
|
.attach(AdHoc::on_attach("webhookey config", move |rocket| {
|
|
|
|
Ok(rocket.manage(config))
|
|
|
|
}));
|
|
|
|
|
|
|
|
let client = Client::new(rocket).unwrap();
|
|
|
|
let response = client
|
|
|
|
.post("/")
|
2021-03-28 03:50:52 +02:00
|
|
|
.header(Header::new(
|
|
|
|
"X-Gitea-Signature",
|
|
|
|
"28175a0035f637f3cbb85afee9f9d319631580e7621cf790cd16ca063a2f820e",
|
|
|
|
))
|
2021-03-20 00:12:01 +01:00
|
|
|
.header(ContentType::JSON)
|
|
|
|
.remote("127.0.0.1:8000".parse().unwrap())
|
2021-03-28 03:50:52 +02:00
|
|
|
.body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap())
|
2021-03-20 00:12:01 +01:00
|
|
|
.dispatch();
|
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
assert_eq!(response.status(), Status::NotFound);
|
2021-03-20 00:12:01 +01:00
|
|
|
|
|
|
|
let response = client
|
|
|
|
.post("/")
|
2021-03-28 03:50:52 +02:00
|
|
|
.header(Header::new("X-Gitea-Signature", "beef"))
|
2021-03-20 00:12:01 +01:00
|
|
|
.header(ContentType::JSON)
|
|
|
|
.remote("127.0.0.1:8000".parse().unwrap())
|
2021-03-28 03:50:52 +02:00
|
|
|
.body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap())
|
2021-03-20 00:12:01 +01:00
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
assert_eq!(response.status(), Status::Unauthorized);
|
|
|
|
|
|
|
|
let response = client
|
|
|
|
.post("/")
|
2021-03-28 03:50:52 +02:00
|
|
|
.header(Header::new(
|
|
|
|
"X-Gitea-Signature",
|
|
|
|
"c5c315d76318362ec129ca629b50b626bba09ad3d7ba4cc0f4c0afe4a90537a0",
|
|
|
|
))
|
2021-03-20 00:12:01 +01:00
|
|
|
.header(ContentType::JSON)
|
|
|
|
.remote("127.0.0.1:8000".parse().unwrap())
|
|
|
|
.body(r#"{ "not_secret": "invalid" "#)
|
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
assert_eq!(response.status(), Status::BadRequest);
|
2021-03-29 02:19:30 +02:00
|
|
|
|
|
|
|
let response = client
|
|
|
|
.post("/")
|
|
|
|
.header(Header::new("X-Gitea-Signature", "foobar"))
|
|
|
|
.header(ContentType::JSON)
|
|
|
|
.remote("127.0.0.1:8000".parse().unwrap())
|
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
assert_eq!(response.status(), Status::Unauthorized);
|
2021-03-20 00:12:01 +01:00
|
|
|
}
|
2021-03-21 15:51:58 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parse_command() {
|
2021-03-28 03:50:52 +02:00
|
|
|
let mut map = HeaderMap::new();
|
|
|
|
map.add_raw("X-Gitea-Event", "something");
|
|
|
|
|
2021-03-21 15:51:58 +01:00
|
|
|
assert_eq!(
|
2021-03-28 03:50:52 +02:00
|
|
|
replace_parameter("command", &map, &serde_json::Value::Null).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
"command"
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-03-28 03:50:52 +02:00
|
|
|
replace_parameter(" command", &map, &serde_json::Value::Null).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
" command"
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-03-28 03:50:52 +02:00
|
|
|
replace_parameter("command ", &map, &serde_json::Value::Null).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
"command "
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-03-28 03:50:52 +02:00
|
|
|
replace_parameter(" command ", &map, &serde_json::Value::Null).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
" command "
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-03-28 03:50:52 +02:00
|
|
|
replace_parameter("command command ", &map, &serde_json::Value::Null).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
"command command "
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-03-28 03:50:52 +02:00
|
|
|
replace_parameter("{{ /foo }} command", &map, &json!({ "foo": "bar" })).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
"bar command"
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-03-28 03:50:52 +02:00
|
|
|
replace_parameter(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
" command bar "
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
replace_parameter(
|
|
|
|
"{{ /foo }} command{{/field1/foo}}",
|
2021-03-28 03:50:52 +02:00
|
|
|
&map,
|
2021-03-21 15:51:58 +01:00
|
|
|
&json!({ "foo": "bar", "field1": { "foo": "baz" } })
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
"bar commandbaz"
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-03-28 03:50:52 +02:00
|
|
|
replace_parameter(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
" command bar "
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
replace_parameter(
|
|
|
|
" {{ /field1/foo }} command",
|
2021-03-28 03:50:52 +02:00
|
|
|
&map,
|
2021-03-21 15:51:58 +01:00
|
|
|
&json!({ "field1": { "foo": "bar" } })
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
" bar command"
|
|
|
|
);
|
2021-03-28 03:50:52 +02:00
|
|
|
|
2021-03-29 04:21:31 +02:00
|
|
|
assert_eq!(
|
|
|
|
replace_parameter(
|
|
|
|
" {{ event }} command",
|
|
|
|
&map,
|
|
|
|
&json!({ "field1": { "foo": "bar" } })
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
" something command"
|
|
|
|
);
|
2021-03-21 15:51:58 +01:00
|
|
|
}
|
2021-03-20 00:12:01 +01:00
|
|
|
}
|