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:
parent
b2205ea5f4
commit
d92e8029f2
3 changed files with 191 additions and 175 deletions
22
src/cli.rs
Normal file
22
src/cli.rs
Normal 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>,
|
||||
}
|
185
src/main.rs
185
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<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
159
src/webhooks.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue