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.
This commit is contained in:
finga 2021-11-11 21:09:47 +01:00
parent b2205ea5f4
commit d92e8029f2
3 changed files with 191 additions and 175 deletions

22
src/cli.rs Normal file
View file

@ -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<String>,
#[clap(subcommand)]
pub command: Option<Command>,
}

View file

@ -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<String>,
#[clap(subcommand)]
command: Option<Command>,
}
use crate::{
cli::Opts,
webhooks::{FilterType, IpFilter, WebhookeyError},
};
#[derive(Debug, Default)]
struct WebhookeyMetrics {
@ -87,38 +53,6 @@ struct WebhookeyMetrics {
commands_failed: Mutex<usize>,
}
#[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<AddrType>),
Deny(Vec<AddrType>),
}
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<ConfigDefault>,
metrics: Option<MetricsConfig>,
hooks: BTreeMap<String, Hook>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct JsonFilter {
pointer: String,
regex: String,
}
impl JsonFilter {
fn evaluate(&self, data: &serde_json::Value) -> Result<bool, WebhookeyError> {
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<FilterType>),
Or(Vec<FilterType>),
#[serde(rename = "json")]
JsonFilter(JsonFilter),
}
impl FilterType {
fn evaluate(
&self,
request: &Request,
data: &serde_json::Value,
) -> Result<bool, WebhookeyError> {
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<String, WebhookeyError> {
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,

159
src/webhooks.rs Normal file
View file

@ -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<AddrType>),
Deny(Vec<AddrType>),
}
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<bool, WebhookeyError> {
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<String, WebhookeyError> {
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<FilterType>),
Or(Vec<FilterType>),
// #[serde(rename = "header")]
// HeaderFilter(HeaderFilter),
#[serde(rename = "json")]
JsonFilter(JsonFilter),
}
impl FilterType {
pub fn evaluate(&self, data: &serde_json::Value) -> Result<bool, WebhookeyError> {
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),
}
}
}