Restructure and minor improvements

In order to keep things together the code was restructured. Some small
improvements such as clippy warnings and the validation of a hook in
regards of the ip filter.
This commit is contained in:
finga 2021-06-02 03:35:43 +02:00 committed by finga
parent d29bfdf88d
commit 65430e65b7
4 changed files with 169 additions and 169 deletions

2
Cargo.lock generated
View file

@ -1242,7 +1242,7 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]] [[package]]
name = "webhookey" name = "webhookey"
version = "0.1.0-rc.2" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "webhookey" name = "webhookey"
version = "0.1.0-rc.2" version = "0.1.0"
authors = ["finga <webhookey@onders.org>"] authors = ["finga <webhookey@onders.org>"]
edition = "2018" edition = "2018"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"

View file

@ -67,12 +67,12 @@ Whereas `<config_dir>` depends on the platform:
#### Hooks #### Hooks
With `hooks` you can configure a sequence of hooks. A single hook With `hooks` you can configure a sequence of hooks. A single hook
consists of the following fields: consists of the following fields:
- command: A command to be executed if a filter matches - `command`: A command to be executed if a filter matches
- allow/deny: An optional parameter to either allow or deny specific - `allow`/`deny`: An optional parameter to either allow or deny
source addresses or ranges. specific source addresses or ranges.
- signature: Name of the HTTP header field containing the signature. - `signature`: Name of the HTTP header field containing the signature.
- secrets: List of secrets. - `secrets`: List of secrets.
- filter: Tree of filters. - `filter`: Tree of filters.
Example: Example:
```yaml ```yaml
@ -163,7 +163,7 @@ Conjunction filters contain lists of other filters.
The `json` filter matches a regular expression on a field from the The `json` filter matches a regular expression on a field from the
received JSON data. received JSON data.
- pointer: Pointer to the JSON field according to [RFC - `pointer`: Pointer to the JSON field according to [RFC
6901](https://tools.ietf.org/html/rfc6901). 6901](https://tools.ietf.org/html/rfc6901).
- regex: Regular expression which has to match the field pointed to - `regex`: Regular expression which has to match the field pointed
by the pointer. to by the pointer.

View file

@ -34,6 +34,26 @@ use std::{
net::{IpAddr, Ipv4Addr, SocketAddr}, net::{IpAddr, Ipv4Addr, SocketAddr},
}; };
#[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 find field refered to in parameter `{0}`")]
InvalidParameterPointer(String),
#[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)] #[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields, untagged)] #[serde(deny_unknown_fields, untagged)]
enum AddrType { enum AddrType {
@ -66,12 +86,6 @@ impl IpFilter {
} }
} }
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Config {
hooks: HashMap<String, Hook>,
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
struct JsonFilter { struct JsonFilter {
@ -171,24 +185,27 @@ struct Hook {
filter: FilterType, filter: FilterType,
} }
#[derive(Debug, Error)] impl Hook {
enum WebhookeyError { fn get_command(
#[error("Could not extract signature from header")] &self,
InvalidSignature, hook_name: &str,
#[error("Unauthorized request from `{0}`")] request: &Request,
Unauthorized(IpAddr), data: &mut serde_json::Value,
#[error("Unmatched hook from `{0}`")] ) -> Result<String> {
UnmatchedHook(IpAddr), trace!("Replacing parameters for command of hook `{}`", hook_name);
#[error("Could not find field refered to in parameter `{0}`")]
InvalidParameterPointer(String), for parameter in get_parameter(&self.command)? {
#[error("Could not evaluate filter request")] let parameter = parameter.trim();
InvalidFilter,
#[error("IO Error")] if let Some(json_value) = data.pointer(parameter) {
Io(std::io::Error), *data.pointer_mut(parameter).ok_or_else(|| {
#[error("Serde Error")] WebhookeyError::InvalidParameterPointer(parameter.to_string())
Serde(serde_json::Error), })? = serde_json::Value::String(get_string(json_value)?);
#[error("Regex Error")] }
Regex(regex::Error), }
replace_parameters(&self.command, &request.headers(), data)
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -196,27 +213,128 @@ struct Hooks {
inner: HashMap<String, String>, inner: HashMap<String, String>,
} }
fn accept_ip(hook_name: &str, client_ip: &IpAddr, ip_filter: &Option<IpFilter>) -> bool { impl Hooks {
match ip_filter { fn get_commands(request: &Request, data: Data) -> Result<Self, WebhookeyError> {
Some(filter) => { let mut buffer = Vec::new();
if filter.validate(client_ip) { let size = data
info!("Allow hook `{}` from {}", &hook_name, &client_ip); .open()
true .read_to_end(&mut buffer)
.map_err(WebhookeyError::Io)?;
info!("Data of size {} received", size);
let config = request.guard::<State<Config>>().unwrap(); // should never fail
let mut valid = false;
let mut result = HashMap::new();
let client_ip = &request
.client_ip()
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
let hooks = config.hooks.iter().filter(|(name, hook)| {
if let Some(ip) = &hook.ip_filter {
return accept_ip(&name, &client_ip, &ip);
} else { } else {
warn!("Deny hook `{}` from {}", &hook_name, &client_ip); info!(
false "Allow hook `{}` from {}, no IP filter was configured",
&name, &client_ip
);
true
}
});
for (hook_name, hook) in hooks {
let signature = request
.headers()
.get_one(&hook.signature)
.ok_or(WebhookeyError::InvalidSignature)?;
let secrets = hook
.secrets
.iter()
.map(|secret| validate_request(&secret, &signature, &buffer));
for secret in secrets {
match secret {
Ok(()) => {
trace!("Valid signature found for hook `{}`", hook_name);
valid = true;
let mut data: serde_json::Value =
serde_json::from_slice(&buffer).map_err(WebhookeyError::Serde)?;
match hook.filter.evaluate(request, &data) {
Ok(true) => match hook.get_command(&hook_name, &request, &mut data) {
Ok(command) => {
info!("Filter for `{}` matched", &hook_name);
result.insert(hook_name.to_string(), command);
break;
}
Err(e) => error!("{}", e),
},
Ok(false) => info!("Filter for `{}` did not match", &hook_name),
Err(error) => {
error!("Could not match filter for `{}`: {}", &hook_name, error)
}
}
}
Err(e) => trace!("Hook `{}` could not validate request: {}", &hook_name, e),
}
} }
} }
None => {
info!( if !valid {
"Allow hook `{}` from {}, no IP filter was configured", return Err(WebhookeyError::Unauthorized(*client_ip));
&hook_name, &client_ip }
);
true Ok(Hooks { inner: result })
}
}
impl FromDataSimple for Hooks {
type Error = WebhookeyError;
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
match Hooks::get_commands(&request, data) {
Ok(hooks) => {
if hooks.inner.is_empty() {
let client_ip = &request
.client_ip()
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
warn!("Unmatched hook from {}", &client_ip);
return Failure((Status::NotFound, WebhookeyError::UnmatchedHook(*client_ip)));
}
Success(hooks)
}
Err(WebhookeyError::Unauthorized(e)) => {
error!("{}", WebhookeyError::Unauthorized(e));
Failure((Status::Unauthorized, WebhookeyError::Unauthorized(e)))
}
Err(e) => {
error!("{}", e);
Failure((Status::BadRequest, e))
}
} }
} }
} }
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Config {
hooks: HashMap<String, Hook>,
}
fn accept_ip(hook_name: &str, client_ip: &IpAddr, ip: &IpFilter) -> bool {
if ip.validate(client_ip) {
info!("Allow hook `{}` from {}", &hook_name, &client_ip);
return true;
}
warn!("Deny hook `{}` from {}", &hook_name, &client_ip);
false
}
fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> { fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
let mut mac = Hmac::<Sha256>::new_varkey(&secret.as_bytes()) let mut mac = Hmac::<Sha256>::new_varkey(&secret.as_bytes())
.map_err(|e| anyhow!("Could not create hasher with secret: {}", e))?; .map_err(|e| anyhow!("Could not create hasher with secret: {}", e))?;
@ -299,124 +417,6 @@ fn get_string(value: &serde_json::Value) -> Result<String, WebhookeyError> {
} }
} }
fn get_command(
hook_name: &str,
hook: &Hook,
request: &Request,
data: &mut serde_json::Value,
) -> Result<String> {
trace!("Replacing parameters for command of hook `{}`", hook_name);
for parameter in get_parameter(&hook.command)? {
let parameter = parameter.trim();
if let Some(json_value) = data.pointer(parameter) {
*data
.pointer_mut(parameter)
.ok_or_else(|| WebhookeyError::InvalidParameterPointer(parameter.to_string()))? =
serde_json::Value::String(get_string(json_value)?);
}
}
replace_parameters(&hook.command, &request.headers(), data)
}
fn get_commands(request: &Request, data: Data) -> Result<Hooks, WebhookeyError> {
let mut buffer = Vec::new();
let size = data
.open()
.read_to_end(&mut buffer)
.map_err(WebhookeyError::Io)?;
info!("Data of size {} received", size);
let config = request.guard::<State<Config>>().unwrap(); // should never fail
let mut valid = false;
let mut result = HashMap::new();
let client_ip = &request
.client_ip()
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
let hooks = config
.hooks
.iter()
.filter(|(name, hook)| accept_ip(&name, &client_ip, &hook.ip_filter));
for (hook_name, hook) in hooks {
let signature = request
.headers()
.get_one(&hook.signature)
.ok_or(WebhookeyError::InvalidSignature)?;
let secrets = hook
.secrets
.iter()
.map(|secret| validate_request(&secret, &signature, &buffer));
for secret in secrets {
match secret {
Ok(()) => {
trace!("Valid signature found for hook `{}`", hook_name);
valid = true;
let mut data: serde_json::Value =
serde_json::from_slice(&buffer).map_err(WebhookeyError::Serde)?;
match hook.filter.evaluate(request, &data) {
Ok(true) => match get_command(&hook_name, &hook, &request, &mut data) {
Ok(command) => {
result.insert(hook_name.to_string(), command);
break;
}
Err(e) => error!("{}", e),
},
Ok(false) => info!("Filter for `{}` did not match", &hook_name),
Err(error) => {
error!("Could not match filter for `{}`: {}", &hook_name, error)
}
}
}
Err(e) => trace!("Hook `{}` could not validate request: {}", &hook_name, e),
}
}
}
if !valid {
return Err(WebhookeyError::Unauthorized(*client_ip));
}
Ok(Hooks { inner: result })
}
impl FromDataSimple for Hooks {
type Error = WebhookeyError;
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
match get_commands(&request, data) {
Ok(hooks) => {
if hooks.inner.is_empty() {
let client_ip = &request
.client_ip()
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
warn!("Unmatched hook from {}", &client_ip);
return Failure((Status::NotFound, WebhookeyError::UnmatchedHook(*client_ip)));
}
Success(hooks)
}
Err(WebhookeyError::Unauthorized(e)) => {
error!("{}", WebhookeyError::Unauthorized(e));
Failure((Status::Unauthorized, WebhookeyError::Unauthorized(e)))
}
Err(e) => {
error!("{}", e);
Failure((Status::BadRequest, e))
}
}
}
}
#[post("/", format = "json", data = "<hooks>")] #[post("/", format = "json", data = "<hooks>")]
fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> { fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> {
info!("Post request received from: {}", address); info!("Post request received from: {}", address);
@ -486,7 +486,7 @@ fn main() -> Result<()> {
) )
.subcommand( .subcommand(
App::new("configtest") App::new("configtest")
.about("Verifies if the configuration can be parsed without errors."), .about("Verifies if the configuration can be parsed without errors"),
) )
.get_matches(); .get_matches();
@ -500,7 +500,7 @@ fn main() -> Result<()> {
if let Some(_) = cli.subcommand_matches("configtest") { if let Some(_) = cli.subcommand_matches("configtest") {
debug!("Configtest succeded."); debug!("Configtest succeded.");
println!("Config is OK"); println!("Config is OK");
return Ok(()) return Ok(());
} }
rocket::ignite() rocket::ignite()