Split from_data() up in smaller pieces

To still be able to handle errors correctly, also regarding the http
status code, `thiserror::Error` is used.
This commit is contained in:
finga 2021-04-03 01:10:50 +02:00
parent 8314214e06
commit 7f143e0b08
4 changed files with 113 additions and 83 deletions

21
Cargo.lock generated
View file

@ -1009,6 +1009,26 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "thiserror"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
dependencies = [
"proc-macro2 1.0.26",
"quote 1.0.9",
"syn 1.0.68",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.43" version = "0.1.43"
@ -1169,6 +1189,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"sha2", "sha2",
"thiserror",
] ]
[[package]] [[package]]

View file

@ -25,3 +25,4 @@ hmac = "0.10"
sha2 = "0.9" sha2 = "0.9"
hex = "0.4" hex = "0.4"
ipnet = { version = "2.3", features = ["serde"] } ipnet = { version = "2.3", features = ["serde"] }
thiserror = "1.0"

View file

@ -7,7 +7,7 @@ actions.
## Build ## Build
### Install Rust ### Install Rust
Install the Rust toolchain from [rustup.rs](https://rustup.rs) Install the Rust toolchain from [rustup.rs](https://rustup.rs).
Further, for Rocket we need to have the nightly toolchain installed: Further, for Rocket we need to have the nightly toolchain installed:
``` sh ``` sh
@ -148,6 +148,6 @@ Each filter must have following fields:
## Configure rocket via config.yml ## Configure rocket via config.yml
## Security ## Security
### https support ### https support
basically supported, but related to "Configure rocket via config.yml". basically supported, but related to "Configure rocket via config.yml".
### Authentication features ### Authentication features
## Use proptest or quickcheck for tests of parsers ## Use proptest or quickcheck for tests of parsers

View file

@ -24,6 +24,7 @@ use rocket::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::Sha256; use sha2::Sha256;
use thiserror::Error;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -71,10 +72,24 @@ struct Filter {
regex: String, regex: String,
} }
#[derive(Debug, Error)]
enum WebhookeyError {
#[error("Could not extract signature from header")]
InvalidHeader,
#[error("Unauthorized request from `{0}`")]
Unauthorized(IpAddr),
#[error("Unmatched hook from `{0}`")]
UnmatchedHook(IpAddr),
#[error("IO Error")]
Io(std::io::Error),
#[error("Serde Error")]
Serde(serde_json::Error),
}
#[derive(Debug)] #[derive(Debug)]
struct Hooks(HashMap<String, String>); struct Hooks(HashMap<String, String>);
fn accepted_ip(hook_name: &str, client_ip: &IpAddr, ip_filter: &Option<IpFilter>) -> bool { fn accept_ip(hook_name: &str, client_ip: &IpAddr, ip_filter: &Option<IpFilter>) -> bool {
match ip_filter { match ip_filter {
Some(IpFilter::Allow(list)) => { Some(IpFilter::Allow(list)) => {
for i in list { for i in list {
@ -215,101 +230,94 @@ fn filter_match(
Ok(None) Ok(None)
} }
impl FromDataSimple for Hooks { fn execute_hooks(request: &Request, data: Data) -> Result<Hooks, WebhookeyError> {
type Error = anyhow::Error; let mut buffer = Vec::new();
let size = data
.open()
.read_to_end(&mut buffer)
.map_err(WebhookeyError::Io)?;
info!("Data of size {} received", size);
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> { let config = request.guard::<State<Config>>().unwrap(); // should never fail
let mut buffer = Vec::new(); let mut valid = false;
match data.open().read_to_end(&mut buffer) { let mut hooks = HashMap::new();
Ok(size) => info!("Data of size {} received", size), let client_ip = &request
Err(e) => { .client_ip()
error!("Could not read to end of data: {}", &e); .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
return Failure((
Status::BadRequest,
anyhow!("Could not read to end of data: {}", &e),
));
}
}
let config = request.guard::<State<Config>>().unwrap(); // should never fail for (hook_name, hook) in &config.hooks {
let mut valid = false; if accept_ip(&hook_name, &client_ip, &hook.ip_filter) {
let mut hooks = HashMap::new(); if let Some(signature) = request.headers().get_one(&hook.signature) {
let client_ip = &request for secret in &hook.secrets {
.client_ip() match validate_request(&secret, &signature, &buffer) {
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); Ok(()) => {
trace!("Valid signature found for hook `{}`", hook_name,);
for (hook_name, hook) in &config.hooks { valid = true;
if accepted_ip(&hook_name, &client_ip, &hook.ip_filter) {
if let Some(signature) = request.headers().get_one(&hook.signature) {
for secret in &hook.secrets {
match validate_request(&secret, &signature, &buffer) {
Ok(()) => {
trace!("Valid signature found for hook `{}`", hook_name,);
valid = true; let data: serde_json::Value =
serde_json::from_slice(&buffer).map_err(WebhookeyError::Serde)?;
let data: serde_json::Value = match serde_json::from_slice(&buffer) for (filter_name, filter) in &hook.filters {
{ match filter_match(
Ok(data) => data, &hook_name,
Err(e) => { &hook,
error!("Could not parse json: {}", e); &filter_name,
return Failure(( &filter,
Status::BadRequest, &request,
anyhow!("Could not parse json: {}", e), &data,
)); ) {
} Ok(Some(command)) => {
}; hooks.insert(hook_name.to_string(), command);
break;
for (filter_name, filter) in &hook.filters {
match filter_match(
&hook_name,
&hook,
&filter_name,
&filter,
&request,
&data,
) {
Ok(Some(command)) => {
hooks.insert(hook_name.to_string(), command);
break;
}
Ok(None) => {}
Err(e) => error!("{}", e),
} }
Ok(None) => {}
Err(e) => error!("{}", e),
} }
} }
Err(e) => {
warn!("Hook `{}` could not validate request: {}", &hook_name, e);
}
} }
Err(e) => trace!("Hook `{}` could not validate request: {}", &hook_name, e),
} }
} else {
error!("Could not extract signature from header");
return Failure((
Status::BadRequest,
anyhow!("Could not extract signature from header"),
));
} }
}
}
if hooks.is_empty() {
if valid {
warn!("Unmatched hook from {}", &client_ip);
return Failure((
Status::NotFound,
anyhow!("Unmatched hook from {}", &client_ip),
));
} else { } else {
error!("Unauthorized request from {}", &client_ip); return Err(WebhookeyError::InvalidHeader);
return Failure((
Status::Unauthorized,
anyhow!("Unauthorized request from {}", &client_ip),
));
} }
} }
}
Success(Hooks(hooks)) if !valid {
return Err(WebhookeyError::Unauthorized(*client_ip));
}
Ok(Hooks(hooks))
}
impl FromDataSimple for Hooks {
type Error = WebhookeyError;
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
match execute_hooks(&request, data) {
Ok(hooks) => {
if hooks.0.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))
}
}
} }
} }