feat(server/webhook): only accept authorised messages
All checks were successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful

Closes kemitix/git-next#47
This commit is contained in:
Paul Campbell 2024-04-14 19:12:51 +01:00
parent b398ac3fd3
commit 64a6b84ee4
4 changed files with 66 additions and 15 deletions

View file

@ -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<WebhookId>, // INFO: if [None] then no webhook is configured
webhook_auth: Option<ulid::Ulid>,
webhook_auth: Option<WebhookAuth>, // INFO: if [None] then no webhook is configured
last_main_commit: Option<forge::Commit>,
last_next_commit: Option<forge::Commit>,
last_dev_commit: Option<forge::Commit>,
@ -168,7 +169,7 @@ impl Handler<StartMonitoring> for RepoActor {
#[derive(Message)]
#[rtype(result = "()")]
pub struct WebhookRegistered(pub WebhookId, pub ulid::Ulid);
pub struct WebhookRegistered(pub WebhookId, pub WebhookAuth);
impl Handler<WebhookRegistered> for RepoActor {
type Result = ();
fn handle(&mut self, msg: WebhookRegistered, _ctx: &mut Self::Context) -> Self::Result {

View file

@ -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<Self, DecodeError> {
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<WebhookMessage> 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");

View file

@ -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> {
WebhookAuth::from_str(&self.authorisation).ok()
}
}

View file

@ -12,24 +12,39 @@ pub async fn start(address: actix::prelude::Recipient<super::message::WebhookMes
.map(move || address.clone())
.and(warp::path::param())
// .and(warp::query::raw())
// .and(warp::header::headers_cloned())
.and(warp::header::headers_cloned())
.and(warp::body::bytes())
.and_then(
|recipient: Recipient<WebhookMessage>,
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()),
}
},
);