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 anyhow::{anyhow, bail, Result};
|
||||||
use clap::{crate_authors, crate_version, AppSettings, Parser};
|
use clap::Parser;
|
||||||
use hmac::{Hmac, Mac, NewMac};
|
use hmac::{Hmac, Mac, NewMac};
|
||||||
use ipnet::IpNet;
|
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use nom::{
|
use nom::{
|
||||||
branch::alt,
|
branch::alt,
|
||||||
|
@ -11,7 +10,6 @@ use nom::{
|
||||||
sequence::delimited,
|
sequence::delimited,
|
||||||
Finish, IResult,
|
Finish, IResult,
|
||||||
};
|
};
|
||||||
use regex::Regex;
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
data::{FromData, ToByteUnit},
|
data::{FromData, ToByteUnit},
|
||||||
futures::TryFutureExt,
|
futures::TryFutureExt,
|
||||||
|
@ -25,7 +23,6 @@ use rocket::{
|
||||||
use run_script::ScriptOptions;
|
use run_script::ScriptOptions;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
|
@ -35,44 +32,13 @@ use std::{
|
||||||
sync::Mutex,
|
sync::Mutex,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
mod cli;
|
||||||
enum WebhookeyError {
|
mod webhooks;
|
||||||
#[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, Parser)]
|
use crate::{
|
||||||
enum Command {
|
cli::Opts,
|
||||||
/// Verifies if the configuration can be parsed without errors
|
webhooks::{FilterType, IpFilter, WebhookeyError},
|
||||||
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>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct WebhookeyMetrics {
|
struct WebhookeyMetrics {
|
||||||
|
@ -87,38 +53,6 @@ struct WebhookeyMetrics {
|
||||||
commands_failed: Mutex<usize>,
|
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)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct MetricsConfig {
|
struct MetricsConfig {
|
||||||
|
@ -129,99 +63,11 @@ struct MetricsConfig {
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct Config {
|
struct Config {
|
||||||
|
// default: Option<ConfigDefault>,
|
||||||
metrics: Option<MetricsConfig>,
|
metrics: Option<MetricsConfig>,
|
||||||
hooks: BTreeMap<String, Hook>,
|
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)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct Hook {
|
struct Hook {
|
||||||
|
@ -300,7 +146,7 @@ impl Hooks {
|
||||||
let data: serde_json::Value =
|
let data: serde_json::Value =
|
||||||
serde_json::from_slice(&buffer).map_err(WebhookeyError::Serde)?;
|
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(true) => match hook.get_command(hook_name, request, &data) {
|
||||||
Ok(command) => {
|
Ok(command) => {
|
||||||
info!("Filter for `{}` matched", &hook_name);
|
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))
|
.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(
|
fn replace_parameters(
|
||||||
input: &str,
|
input: &str,
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
|
@ -656,6 +490,7 @@ async fn main() -> Result<()> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::webhooks::{AddrType, JsonFilter};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{ContentType, Header},
|
http::{ContentType, Header},
|
||||||
local::asynchronous::Client,
|
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