feat(server/webhook): only accept authorised messages
Closes kemitix/git-next#47
This commit is contained in:
parent
b398ac3fd3
commit
64a6b84ee4
4 changed files with 66 additions and 15 deletions
|
@ -1,13 +1,14 @@
|
||||||
mod branch;
|
mod branch;
|
||||||
mod config;
|
mod config;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
mod webhook;
|
pub mod webhook;
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use kxio::network::Network;
|
use kxio::network::Network;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::server::{
|
use crate::server::{
|
||||||
|
actors::repo::webhook::WebhookAuth,
|
||||||
config::{RepoConfig, RepoDetails, Webhook},
|
config::{RepoConfig, RepoDetails, Webhook},
|
||||||
forge,
|
forge,
|
||||||
git::Git,
|
git::Git,
|
||||||
|
@ -19,7 +20,7 @@ pub struct RepoActor {
|
||||||
details: RepoDetails,
|
details: RepoDetails,
|
||||||
webhook: Webhook,
|
webhook: Webhook,
|
||||||
webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured
|
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_main_commit: Option<forge::Commit>,
|
||||||
last_next_commit: Option<forge::Commit>,
|
last_next_commit: Option<forge::Commit>,
|
||||||
last_dev_commit: Option<forge::Commit>,
|
last_dev_commit: Option<forge::Commit>,
|
||||||
|
@ -168,7 +169,7 @@ impl Handler<StartMonitoring> for RepoActor {
|
||||||
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
#[rtype(result = "()")]
|
#[rtype(result = "()")]
|
||||||
pub struct WebhookRegistered(pub WebhookId, pub ulid::Ulid);
|
pub struct WebhookRegistered(pub WebhookId, pub WebhookAuth);
|
||||||
impl Handler<WebhookRegistered> for RepoActor {
|
impl Handler<WebhookRegistered> for RepoActor {
|
||||||
type Result = ();
|
type Result = ();
|
||||||
fn handle(&mut self, msg: WebhookRegistered, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: WebhookRegistered, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use kxio::network::{self, json};
|
use kxio::network::{self, json};
|
||||||
use tracing::{debug, info, warn};
|
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::{
|
use crate::server::{
|
||||||
actors::{
|
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(
|
pub async fn unregister(
|
||||||
webhook_id: WebhookId,
|
webhook_id: WebhookId,
|
||||||
repo_details: crate::server::config::RepoDetails,
|
repo_details: crate::server::config::RepoDetails,
|
||||||
|
@ -82,10 +106,10 @@ pub async fn register(
|
||||||
let webhook_url = webhook.url();
|
let webhook_url = webhook.url();
|
||||||
let repo_alias = repo_details.name;
|
let repo_alias = repo_details.name;
|
||||||
let headers = network::NetRequestHeaders::new().with("Content-Type", "application/json");
|
let headers = network::NetRequestHeaders::new().with("Content-Type", "application/json");
|
||||||
let authorisation = ulid::Ulid::new();
|
let authorisation = WebhookAuth::generate();
|
||||||
let body = json!({
|
let body = json!({
|
||||||
"active": true,
|
"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()),
|
"branch_filter": format!("{{{},{},{}}}", repo_config.branches().main(), repo_config.branches().next(), repo_config.branches().dev()),
|
||||||
"config": {
|
"config": {
|
||||||
"content_type": "json",
|
"content_type": "json",
|
||||||
|
@ -130,6 +154,9 @@ impl Handler<WebhookMessage> for RepoActor {
|
||||||
|
|
||||||
#[allow(clippy::cognitive_complexity)] // TODO: (#49) reduce complexity
|
#[allow(clippy::cognitive_complexity)] // TODO: (#49) reduce complexity
|
||||||
fn handle(&mut self, msg: WebhookMessage, ctx: &mut Self::Context) -> Self::Result {
|
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 id = msg.id();
|
||||||
let body = msg.body();
|
let body = msg.body();
|
||||||
debug!(?id, "RepoActor received message");
|
debug!(?id, "RepoActor received message");
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
|
use crate::server::actors::repo::webhook::WebhookAuth;
|
||||||
|
|
||||||
#[derive(Message, Debug, Clone)]
|
#[derive(Message, Debug, Clone)]
|
||||||
#[rtype(result = "()")]
|
#[rtype(result = "()")]
|
||||||
pub struct WebhookMessage {
|
pub struct WebhookMessage {
|
||||||
id: String,
|
id: String,
|
||||||
path: String,
|
path: String,
|
||||||
|
authorisation: String,
|
||||||
// query: String,
|
// query: String,
|
||||||
// headers: warp::http::HeaderMap,
|
// headers: warp::http::HeaderMap,
|
||||||
body: String,
|
body: String,
|
||||||
|
@ -17,6 +20,7 @@ impl WebhookMessage {
|
||||||
// query: String,
|
// query: String,
|
||||||
// headers: warp::http::HeaderMap,
|
// headers: warp::http::HeaderMap,
|
||||||
body: String,
|
body: String,
|
||||||
|
authorisation: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
@ -24,6 +28,7 @@ impl WebhookMessage {
|
||||||
// query,
|
// query,
|
||||||
// headers,
|
// headers,
|
||||||
body,
|
body,
|
||||||
|
authorisation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const fn id(&self) -> &String {
|
pub const fn id(&self) -> &String {
|
||||||
|
@ -41,4 +46,7 @@ impl WebhookMessage {
|
||||||
pub const fn body(&self) -> &String {
|
pub const fn body(&self) -> &String {
|
||||||
&self.body
|
&self.body
|
||||||
}
|
}
|
||||||
|
pub fn authorisation(&self) -> Option<WebhookAuth> {
|
||||||
|
WebhookAuth::from_str(&self.authorisation).ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,24 +12,39 @@ pub async fn start(address: actix::prelude::Recipient<super::message::WebhookMes
|
||||||
.map(move || address.clone())
|
.map(move || address.clone())
|
||||||
.and(warp::path::param())
|
.and(warp::path::param())
|
||||||
// .and(warp::query::raw())
|
// .and(warp::query::raw())
|
||||||
// .and(warp::header::headers_cloned())
|
.and(warp::header::headers_cloned())
|
||||||
.and(warp::body::bytes())
|
.and(warp::body::bytes())
|
||||||
.and_then(
|
.and_then(
|
||||||
|recipient: Recipient<WebhookMessage>,
|
|recipient: Recipient<WebhookMessage>,
|
||||||
path,
|
path,
|
||||||
// query: String,
|
// query: String,
|
||||||
// headers: warp::http::HeaderMap,
|
headers: warp::http::HeaderMap,
|
||||||
body: bytes::Bytes| async move {
|
body: bytes::Bytes| async move {
|
||||||
let bytes = body.to_vec();
|
let bytes = body.to_vec();
|
||||||
let request_data = String::from_utf8_lossy(&bytes).to_string();
|
let request_data = String::from_utf8_lossy(&bytes).to_string();
|
||||||
let id = ulid::Ulid::new().to_string();
|
let id = ulid::Ulid::new().to_string();
|
||||||
|
match headers.get("Authorization") {
|
||||||
|
Some(auhorisation) => {
|
||||||
debug!(id, path, "Received webhook");
|
debug!(id, path, "Received webhook");
|
||||||
let message =
|
let authorisation = auhorisation
|
||||||
WebhookMessage::new(id, path, /* query, headers, */ request_data);
|
.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
|
recipient
|
||||||
.try_send(message)
|
.try_send(message)
|
||||||
.map(|_| warp::reply::with_status("OK", warp::http::StatusCode::OK))
|
.map(|_| warp::reply::with_status("OK", warp::http::StatusCode::OK))
|
||||||
.map_err(|_| warp::reject::reject())
|
.map_err(|_| warp::reject())
|
||||||
|
}
|
||||||
|
_ => Err(warp::reject()),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue