diff --git a/Cargo.lock b/Cargo.lock index 0df1cf6..b72e6ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1873,6 +1873,7 @@ name = "webhookey" version = "0.1.5" dependencies = [ "anyhow", + "base64", "clap", "dirs", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index c01a14d..f968da3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ ipnet = { version = "2.3", features = ["serde"] } thiserror = "1.0" run_script = "0.9" clap = "3.0.0-beta.5" +base64 = "0.13" [package.metadata.deb] extended-description = "Webhookey receives requests in form of a so called Webhook as for example sent by Gitea. Those requests are matched against configured filters, if a filter matches, values from the header and the body can be passed to scripts as parameters which are then executed subsequently." diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..28d39a5 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,74 @@ +use crate::WebhookeyError; +use base64; +use rocket::{ + http::Status, + outcome::Outcome, + request::{self, FromRequest}, + Request, +}; +use serde::{Deserialize, Serialize}; +use std::net::{IpAddr, Ipv4Addr}; + +fn decode_to_creds>(base64_encoded: T) -> Option<(String, String)> { + let decoded_creds = match base64::decode(base64_encoded.into()) { + Ok(cred_bytes) => String::from_utf8(cred_bytes).unwrap(), + Err(_) => return None, + }; + + if let Some((username, password)) = decoded_creds.split_once(":") { + Some((username.to_string(), password.to_string())) + } else { + None + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct BasicAuth { + username: String, + password: String, +} + +impl BasicAuth { + pub fn new>(auth_header: T) -> Option { + let key = auth_header.into(); + + if key.len() < 7 || &key[..6] != "Basic " { + return None; + } + + let (username, password) = decode_to_creds(&key[6..])?; + Some(Self { username, password }) + } +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for BasicAuth { + type Error = WebhookeyError; + + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + let keys: Vec<_> = request.headers().get("Authorization").collect(); + + match keys.len() { + 0 => Outcome::Forward(()), + 1 => match BasicAuth::new(keys[0]) { + Some(auth_header) => Outcome::Success(auth_header), + None => Outcome::Failure(( + Status::Unauthorized, + WebhookeyError::Unauthorized( + request + .client_ip() + .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)), + ), + )), + }, + _ => Outcome::Failure(( + Status::Unauthorized, + WebhookeyError::Unauthorized( + request + .client_ip() + .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)), + ), + )), + } + } +} diff --git a/src/config.rs b/src/config.rs index 64b0eb4..bf1bc3a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use crate::{filters::IpFilter, hooks::Hook}; +use crate::{auth::BasicAuth, filters::IpFilter, hooks::Hook}; use anyhow::{bail, Result}; use log::info; use serde::{Deserialize, Serialize}; @@ -15,6 +15,7 @@ pub struct MetricsConfig { #[serde(deny_unknown_fields)] pub struct Config { pub metrics: Option, + pub auth: Option, pub hooks: BTreeMap, } diff --git a/src/hooks.rs b/src/hooks.rs index 1184555..c3a3711 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -1,4 +1,5 @@ use crate::{ + auth::BasicAuth, filters::{FilterType, IpFilter}, Config, Metrics, WebhookeyError, }; @@ -52,6 +53,7 @@ fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> { pub struct Hook { command: String, signature: String, + auth: Option, ip_filter: Option, secrets: Vec, filter: FilterType, diff --git a/src/main.rs b/src/main.rs index 65c64c4..f6f7f03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod auth; mod cli; mod config; mod filters;