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]]
name = "webhookey"
version = "0.1.0-rc.2"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",

View file

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

View file

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

View file

@ -34,6 +34,26 @@ use std::{
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)]
#[serde(deny_unknown_fields, untagged)]
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)]
#[serde(deny_unknown_fields)]
struct JsonFilter {
@ -171,24 +185,27 @@ struct Hook {
filter: FilterType,
}
#[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),
impl Hook {
fn get_command(
&self,
hook_name: &str,
request: &Request,
data: &mut serde_json::Value,
) -> Result<String> {
trace!("Replacing parameters for command of hook `{}`", hook_name);
for parameter in get_parameter(&self.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(&self.command, &request.headers(), data)
}
}
#[derive(Debug)]
@ -196,27 +213,128 @@ struct Hooks {
inner: HashMap<String, String>,
}
fn accept_ip(hook_name: &str, client_ip: &IpAddr, ip_filter: &Option<IpFilter>) -> bool {
match ip_filter {
Some(filter) => {
if filter.validate(client_ip) {
info!("Allow hook `{}` from {}", &hook_name, &client_ip);
true
impl Hooks {
fn get_commands(request: &Request, data: Data) -> Result<Self, 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)| {
if let Some(ip) = &hook.ip_filter {
return accept_ip(&name, &client_ip, &ip);
} else {
warn!("Deny hook `{}` from {}", &hook_name, &client_ip);
false
info!(
"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!(
"Allow hook `{}` from {}, no IP filter was configured",
&hook_name, &client_ip
);
true
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 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<()> {
let mut mac = Hmac::<Sha256>::new_varkey(&secret.as_bytes())
.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>")]
fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> {
info!("Post request received from: {}", address);
@ -486,7 +486,7 @@ fn main() -> Result<()> {
)
.subcommand(
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();
@ -500,7 +500,7 @@ fn main() -> Result<()> {
if let Some(_) = cli.subcommand_matches("configtest") {
debug!("Configtest succeded.");
println!("Config is OK");
return Ok(())
return Ok(());
}
rocket::ignite()