diff --git a/Cargo.lock b/Cargo.lock index fee3e4c..53a9ba2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1009,6 +1009,26 @@ dependencies = [ "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]] name = "time" version = "0.1.43" @@ -1169,6 +1189,7 @@ dependencies = [ "serde_json", "serde_yaml", "sha2", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8f3a559..c76b30d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ hmac = "0.10" sha2 = "0.9" hex = "0.4" ipnet = { version = "2.3", features = ["serde"] } +thiserror = "1.0" diff --git a/README.md b/README.md index acf82de..25b32f2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ actions. ## Build ### 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: ``` sh @@ -148,6 +148,6 @@ Each filter must have following fields: ## Configure rocket via config.yml ## Security ### https support - basically supported, but related to "Configure rocket via config.yml". +basically supported, but related to "Configure rocket via config.yml". ### Authentication features ## Use proptest or quickcheck for tests of parsers diff --git a/src/main.rs b/src/main.rs index 762a0c3..1d2d6db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ use rocket::{ }; use serde::{Deserialize, Serialize}; use sha2::Sha256; +use thiserror::Error; use std::{ collections::HashMap, @@ -71,10 +72,24 @@ struct Filter { 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)] struct Hooks(HashMap); -fn accepted_ip(hook_name: &str, client_ip: &IpAddr, ip_filter: &Option) -> bool { +fn accept_ip(hook_name: &str, client_ip: &IpAddr, ip_filter: &Option) -> bool { match ip_filter { Some(IpFilter::Allow(list)) => { for i in list { @@ -215,101 +230,94 @@ fn filter_match( Ok(None) } -impl FromDataSimple for Hooks { - type Error = anyhow::Error; +fn execute_hooks(request: &Request, data: Data) -> Result { + 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 { - let mut buffer = Vec::new(); - match data.open().read_to_end(&mut buffer) { - Ok(size) => info!("Data of size {} received", size), - Err(e) => { - error!("Could not read to end of data: {}", &e); - return Failure(( - Status::BadRequest, - anyhow!("Could not read to end of data: {}", &e), - )); - } - } + let config = request.guard::>().unwrap(); // should never fail + let mut valid = false; + let mut hooks = HashMap::new(); + let client_ip = &request + .client_ip() + .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); - let config = request.guard::>().unwrap(); // should never fail - let mut valid = false; - let mut hooks = HashMap::new(); - let client_ip = &request - .client_ip() - .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); + for (hook_name, hook) in &config.hooks { + if accept_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,); - for (hook_name, hook) in &config.hooks { - 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; - 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) - { - Ok(data) => data, - Err(e) => { - error!("Could not parse json: {}", e); - return Failure(( - Status::BadRequest, - anyhow!("Could not parse json: {}", e), - )); - } - }; - - 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), + 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), } } - 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 { - error!("Unauthorized request from {}", &client_ip); - return Failure(( - Status::Unauthorized, - anyhow!("Unauthorized request from {}", &client_ip), - )); + return Err(WebhookeyError::InvalidHeader); } } + } - 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 { + 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)) + } + } } }