From d92e8029f2f1cc91dd987ee63594752f1bd0c0f6 Mon Sep 17 00:00:00 2001 From: finga Date: Thu, 11 Nov 2021 21:09:47 +0100 Subject: [PATCH] Break up code into multiple files In order to increase readability, maintainability and maybe a future independence regarding web frameworks move code to new files. --- src/cli.rs | 22 ++++++ src/main.rs | 185 +++--------------------------------------------- src/webhooks.rs | 159 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 175 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/webhooks.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..63c024d --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,22 @@ +use clap::{crate_authors, crate_version, AppSettings, Parser}; + +#[derive(Debug, Parser)] +pub enum Command { + /// Verifies if the configuration can be parsed without errors + Configtest, +} + +#[derive(Debug, Parser)] +#[clap( + version = crate_version!(), + author = crate_authors!(", "), + global_setting = AppSettings::InferSubcommands, + global_setting = AppSettings::PropagateVersion, +)] +pub struct Opts { + /// Provide a path to the configuration file + #[clap(short, long, value_name = "FILE")] + pub config: Option, + #[clap(subcommand)] + pub command: Option, +} diff --git a/src/main.rs b/src/main.rs index 8a870b1..ca3daec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ use anyhow::{anyhow, bail, Result}; -use clap::{crate_authors, crate_version, AppSettings, Parser}; +use clap::Parser; use hmac::{Hmac, Mac, NewMac}; -use ipnet::IpNet; use log::{debug, error, info, trace, warn}; use nom::{ branch::alt, @@ -11,7 +10,6 @@ use nom::{ sequence::delimited, Finish, IResult, }; -use regex::Regex; use rocket::{ data::{FromData, ToByteUnit}, futures::TryFutureExt, @@ -25,7 +23,6 @@ use rocket::{ use run_script::ScriptOptions; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use thiserror::Error; use std::{ collections::BTreeMap, @@ -35,44 +32,13 @@ use std::{ sync::Mutex, }; -#[derive(Debug, Error)] -enum WebhookeyError { - #[error("Could not extract signature from header")] - InvalidSignature, - #[error("Unauthorized request from `{0}`")] - Unauthorized(IpAddr), - #[error("Unmatched hook from `{0}`")] - UnmatchedHook(IpAddr), - #[error("Could not evaluate filter request")] - InvalidFilter, - #[error("IO Error")] - Io(std::io::Error), - #[error("Serde Error")] - Serde(serde_json::Error), - #[error("Regex Error")] - Regex(regex::Error), -} +mod cli; +mod webhooks; -#[derive(Debug, Parser)] -enum Command { - /// Verifies if the configuration can be parsed without errors - Configtest, -} - -#[derive(Debug, Parser)] -#[clap( - version = crate_version!(), - author = crate_authors!(", "), - global_setting = AppSettings::InferSubcommands, - global_setting = AppSettings::PropagateVersion, -)] -struct Opts { - /// Provide a path to the configuration file - #[clap(short, long, value_name = "FILE")] - config: Option, - #[clap(subcommand)] - command: Option, -} +use crate::{ + cli::Opts, + webhooks::{FilterType, IpFilter, WebhookeyError}, +}; #[derive(Debug, Default)] struct WebhookeyMetrics { @@ -87,38 +53,6 @@ struct WebhookeyMetrics { commands_failed: Mutex, } -#[derive(Debug, Deserialize, Serialize)] -#[serde(deny_unknown_fields, untagged)] -enum AddrType { - IpAddr(IpAddr), - IpNet(IpNet), -} - -impl AddrType { - fn matches(&self, client_ip: &IpAddr) -> bool { - match self { - AddrType::IpAddr(addr) => addr == client_ip, - AddrType::IpNet(net) => net.contains(client_ip), - } - } -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(deny_unknown_fields, rename_all = "lowercase")] -enum IpFilter { - Allow(Vec), - Deny(Vec), -} - -impl IpFilter { - fn validate(&self, client_ip: &IpAddr) -> bool { - match self { - IpFilter::Allow(list) => list.iter().any(|i| i.matches(client_ip)), - IpFilter::Deny(list) => !list.iter().any(|i| i.matches(client_ip)), - } - } -} - #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct MetricsConfig { @@ -129,99 +63,11 @@ struct MetricsConfig { #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct Config { + // default: Option, metrics: Option, hooks: BTreeMap, } -#[derive(Debug, Deserialize, Serialize)] -#[serde(deny_unknown_fields)] -struct JsonFilter { - pointer: String, - regex: String, -} - -impl JsonFilter { - fn evaluate(&self, data: &serde_json::Value) -> Result { - trace!( - "Matching `{}` on `{}` from received json", - &self.regex, - &self.pointer, - ); - - let regex = Regex::new(&self.regex).map_err(WebhookeyError::Regex)?; - - if let Some(value) = data.pointer(&self.pointer) { - let value = get_string(value)?; - - if regex.is_match(&value) { - debug!("Regex `{}` for `{}` matches", &self.regex, &self.pointer); - - return Ok(true); - } - } - - debug!( - "Regex `{}` for `{}` does not match", - &self.regex, &self.pointer - ); - - Ok(false) - } -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(deny_unknown_fields, rename_all = "lowercase")] -enum FilterType { - And(Vec), - Or(Vec), - #[serde(rename = "json")] - JsonFilter(JsonFilter), -} - -impl FilterType { - fn evaluate( - &self, - request: &Request, - data: &serde_json::Value, - ) -> Result { - match self { - FilterType::And(filters) => { - let (results, errors): (Vec<_>, Vec<_>) = filters - .iter() - .map(|filter| filter.evaluate(request, data)) - .partition(Result::is_ok); - - if errors.is_empty() { - Ok(results.iter().all(|r| *r.as_ref().unwrap())) // should never fail - } else { - errors.iter().for_each(|e| { - error!("Could not evaluate Filter: {}", e.as_ref().unwrap_err()) - }); - - Err(WebhookeyError::InvalidFilter) - } - } - FilterType::Or(filters) => { - let (results, errors): (Vec<_>, Vec<_>) = filters - .iter() - .map(|filter| filter.evaluate(request, data)) - .partition(Result::is_ok); - - if errors.is_empty() { - Ok(results.iter().any(|r| *r.as_ref().unwrap())) // should never fail - } else { - errors.iter().for_each(|e| { - error!("Could not evaluate Filter: {}", e.as_ref().unwrap_err()) - }); - - Err(WebhookeyError::InvalidFilter) - } - } - FilterType::JsonFilter(filter) => filter.evaluate(data), - } - } -} - #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct Hook { @@ -300,7 +146,7 @@ impl Hooks { let data: serde_json::Value = serde_json::from_slice(&buffer).map_err(WebhookeyError::Serde)?; - match hook.filter.evaluate(request, &data) { + match hook.filter.evaluate(&data) { Ok(true) => match hook.get_command(hook_name, request, &data) { Ok(command) => { info!("Filter for `{}` matched", &hook_name); @@ -366,18 +212,6 @@ fn get_value_from_pointer<'a>(data: &'a serde_json::Value, pointer: &'a str) -> .ok_or_else(|| anyhow!("Could not convert value `{}` to string", value)) } -fn get_string(value: &serde_json::Value) -> Result { - match &value { - serde_json::Value::Bool(bool) => Ok(bool.to_string()), - serde_json::Value::Number(number) => Ok(number.to_string()), - serde_json::Value::String(string) => Ok(string.as_str().to_string()), - x => { - error!("Could not get string from: {:?}", x); - unimplemented!() - } - } -} - fn replace_parameters( input: &str, headers: &HeaderMap, @@ -656,6 +490,7 @@ async fn main() -> Result<()> { #[cfg(test)] mod tests { use super::*; + use crate::webhooks::{AddrType, JsonFilter}; use rocket::{ http::{ContentType, Header}, local::asynchronous::Client, diff --git a/src/webhooks.rs b/src/webhooks.rs new file mode 100644 index 0000000..fee01c7 --- /dev/null +++ b/src/webhooks.rs @@ -0,0 +1,159 @@ +use anyhow::Result; +use ipnet::IpNet; +use log::{debug, error, trace}; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::net::IpAddr; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum WebhookeyError { + #[error("Could not extract signature from header")] + InvalidSignature, + #[error("Unauthorized request from `{0}`")] + Unauthorized(IpAddr), + #[error("Unmatched hook from `{0}`")] + UnmatchedHook(IpAddr), + #[error("Could not evaluate filter request")] + InvalidFilter, + #[error("IO Error")] + Io(std::io::Error), + #[error("Serde Error")] + Serde(serde_json::Error), + #[error("Regex Error")] + Regex(regex::Error), +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields, untagged)] +pub enum AddrType { + IpAddr(IpAddr), + IpNet(IpNet), +} + +impl AddrType { + pub fn matches(&self, client_ip: &IpAddr) -> bool { + match self { + AddrType::IpAddr(addr) => addr == client_ip, + AddrType::IpNet(net) => net.contains(client_ip), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields, rename_all = "lowercase")] +pub enum IpFilter { + Allow(Vec), + Deny(Vec), +} + +impl IpFilter { + pub fn validate(&self, client_ip: &IpAddr) -> bool { + match self { + IpFilter::Allow(list) => list.iter().any(|i| i.matches(client_ip)), + IpFilter::Deny(list) => !list.iter().any(|i| i.matches(client_ip)), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct JsonFilter { + pub pointer: String, + pub regex: String, +} + +impl JsonFilter { + pub fn evaluate(&self, data: &serde_json::Value) -> Result { + trace!( + "Matching `{}` on `{}` from received json", + &self.regex, + &self.pointer, + ); + + let regex = Regex::new(&self.regex).map_err(WebhookeyError::Regex)?; + // let value = self.get_string(data)?; + + // if let Some(value) = self.get_string() {data.pointer(&self.pointer) { + // let value = get_string(); + + if let Some(value) = data.pointer(&self.pointer) { + if regex.is_match(&self.get_string(&value)?) { + debug!("Regex `{}` for `{}` matches", &self.regex, &self.pointer); + + return Ok(true); + } + } + + debug!( + "Regex `{}` for `{}` does not match", + &self.regex, &self.pointer + ); + + Ok(false) + } + + fn get_string(&self, data: &serde_json::Value) -> Result { + match &data { + serde_json::Value::Bool(bool) => Ok(bool.to_string()), + serde_json::Value::Number(number) => Ok(number.to_string()), + serde_json::Value::String(string) => Ok(string.as_str().to_string()), + x => { + error!("Could not get string from: {:?}", x); + unimplemented!() + } + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields, rename_all = "lowercase")] +pub enum FilterType { + And(Vec), + Or(Vec), + // #[serde(rename = "header")] + // HeaderFilter(HeaderFilter), + #[serde(rename = "json")] + JsonFilter(JsonFilter), +} + +impl FilterType { + pub fn evaluate(&self, data: &serde_json::Value) -> Result { + match self { + FilterType::And(filters) => { + let (results, errors): (Vec<_>, Vec<_>) = filters + .iter() + .map(|filter| filter.evaluate(data)) + .partition(Result::is_ok); + + if errors.is_empty() { + Ok(results.iter().all(|r| *r.as_ref().unwrap())) // should never fail + } else { + errors.iter().for_each(|e| { + error!("Could not evaluate Filter: {}", e.as_ref().unwrap_err()) + }); + + Err(WebhookeyError::InvalidFilter) + } + } + FilterType::Or(filters) => { + let (results, errors): (Vec<_>, Vec<_>) = filters + .iter() + .map(|filter| filter.evaluate(data)) + .partition(Result::is_ok); + + if errors.is_empty() { + Ok(results.iter().any(|r| *r.as_ref().unwrap())) // should never fail + } else { + errors.iter().for_each(|e| { + error!("Could not evaluate Filter: {}", e.as_ref().unwrap_err()) + }); + + Err(WebhookeyError::InvalidFilter) + } + } + // FilterType::HeaderFilter(filter) => todo!(), + FilterType::JsonFilter(filter) => filter.evaluate(data), + } + } +}