From 64a6b84ee4e803a2f73a6c8f657e826fd48ca7a5 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sun, 14 Apr 2024 19:12:51 +0100 Subject: [PATCH] feat(server/webhook): only accept authorised messages Closes kemitix/git-next#47 --- src/server/actors/repo/mod.rs | 7 +++--- src/server/actors/repo/webhook.rs | 33 +++++++++++++++++++++++++--- src/server/actors/webhook/message.rs | 8 +++++++ src/server/actors/webhook/server.rs | 33 ++++++++++++++++++++-------- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/server/actors/repo/mod.rs b/src/server/actors/repo/mod.rs index 6dd7f37..a634f1b 100644 --- a/src/server/actors/repo/mod.rs +++ b/src/server/actors/repo/mod.rs @@ -1,13 +1,14 @@ mod branch; mod config; pub mod status; -mod webhook; +pub mod webhook; use actix::prelude::*; use kxio::network::Network; use tracing::{info, warn}; use crate::server::{ + actors::repo::webhook::WebhookAuth, config::{RepoConfig, RepoDetails, Webhook}, forge, git::Git, @@ -19,7 +20,7 @@ pub struct RepoActor { details: RepoDetails, webhook: Webhook, webhook_id: Option, // INFO: if [None] then no webhook is configured - webhook_auth: Option, + webhook_auth: Option, // INFO: if [None] then no webhook is configured last_main_commit: Option, last_next_commit: Option, last_dev_commit: Option, @@ -168,7 +169,7 @@ impl Handler for RepoActor { #[derive(Message)] #[rtype(result = "()")] -pub struct WebhookRegistered(pub WebhookId, pub ulid::Ulid); +pub struct WebhookRegistered(pub WebhookId, pub WebhookAuth); impl Handler for RepoActor { type Result = (); fn handle(&mut self, msg: WebhookRegistered, _ctx: &mut Self::Context) -> Self::Result { diff --git a/src/server/actors/repo/webhook.rs b/src/server/actors/repo/webhook.rs index f51fcb4..8d4677d 100644 --- a/src/server/actors/repo/webhook.rs +++ b/src/server/actors/repo/webhook.rs @@ -1,8 +1,9 @@ use actix::prelude::*; use kxio::network::{self, json}; use tracing::{debug, info, warn}; +use ulid::DecodeError; -use std::{fmt::Display, ops::Deref}; +use std::{fmt::Display, ops::Deref, str::FromStr}; use crate::server::{ actors::{ @@ -33,6 +34,29 @@ impl Display for WebhookId { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WebhookAuth(ulid::Ulid); +impl WebhookAuth { + pub fn from_str(authorisation: &str) -> Result { + let id = ulid::Ulid::from_str(authorisation); + Ok(Self(id?)) + } + + fn generate() -> Self { + Self(ulid::Ulid::new()) + } + + fn header_value(&self) -> String { + format!("Basic {}", self.0.to_string()) + } +} +impl Deref for WebhookAuth { + type Target = ulid::Ulid; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + pub async fn unregister( webhook_id: WebhookId, repo_details: crate::server::config::RepoDetails, @@ -82,10 +106,10 @@ pub async fn register( let webhook_url = webhook.url(); let repo_alias = repo_details.name; let headers = network::NetRequestHeaders::new().with("Content-Type", "application/json"); - let authorisation = ulid::Ulid::new(); + let authorisation = WebhookAuth::generate(); let body = json!({ "active": true, - "authorization_header": format!("Basic {}", authorisation), + "authorization_header": authorisation.header_value(), "branch_filter": format!("{{{},{},{}}}", repo_config.branches().main(), repo_config.branches().next(), repo_config.branches().dev()), "config": { "content_type": "json", @@ -130,6 +154,9 @@ impl Handler for RepoActor { #[allow(clippy::cognitive_complexity)] // TODO: (#49) reduce complexity fn handle(&mut self, msg: WebhookMessage, ctx: &mut Self::Context) -> Self::Result { + if msg.authorisation() != self.webhook_auth { + return; // invalid auth + } let id = msg.id(); let body = msg.body(); debug!(?id, "RepoActor received message"); diff --git a/src/server/actors/webhook/message.rs b/src/server/actors/webhook/message.rs index d392ee7..ce89689 100644 --- a/src/server/actors/webhook/message.rs +++ b/src/server/actors/webhook/message.rs @@ -1,11 +1,14 @@ // use actix::prelude::*; +use crate::server::actors::repo::webhook::WebhookAuth; + #[derive(Message, Debug, Clone)] #[rtype(result = "()")] pub struct WebhookMessage { id: String, path: String, + authorisation: String, // query: String, // headers: warp::http::HeaderMap, body: String, @@ -17,6 +20,7 @@ impl WebhookMessage { // query: String, // headers: warp::http::HeaderMap, body: String, + authorisation: String, ) -> Self { Self { id, @@ -24,6 +28,7 @@ impl WebhookMessage { // query, // headers, body, + authorisation, } } pub const fn id(&self) -> &String { @@ -41,4 +46,7 @@ impl WebhookMessage { pub const fn body(&self) -> &String { &self.body } + pub fn authorisation(&self) -> Option { + WebhookAuth::from_str(&self.authorisation).ok() + } } diff --git a/src/server/actors/webhook/server.rs b/src/server/actors/webhook/server.rs index 51d8c2b..1973393 100644 --- a/src/server/actors/webhook/server.rs +++ b/src/server/actors/webhook/server.rs @@ -12,24 +12,39 @@ pub async fn start(address: actix::prelude::Recipient, path, // query: String, - // headers: warp::http::HeaderMap, + headers: warp::http::HeaderMap, body: bytes::Bytes| async move { let bytes = body.to_vec(); let request_data = String::from_utf8_lossy(&bytes).to_string(); let id = ulid::Ulid::new().to_string(); - debug!(id, path, "Received webhook"); - let message = - WebhookMessage::new(id, path, /* query, headers, */ request_data); - recipient - .try_send(message) - .map(|_| warp::reply::with_status("OK", warp::http::StatusCode::OK)) - .map_err(|_| warp::reject::reject()) + match headers.get("Authorization") { + Some(auhorisation) => { + debug!(id, path, "Received webhook"); + let authorisation = auhorisation + .to_str() + .map_err(|_| warp::reject())? // valid characters + .strip_prefix("Basic ") + .ok_or_else(warp::reject)? // must start with "Basic " + .to_string(); + let message = WebhookMessage::new( + id, + path, + /* query, headers, */ request_data, + authorisation, + ); + recipient + .try_send(message) + .map(|_| warp::reply::with_status("OK", warp::http::StatusCode::OK)) + .map_err(|_| warp::reject()) + } + _ => Err(warp::reject()), + } }, );