fix(server): invalid webhook authorisations
Parameters had been passed in wrong order. Added strong types to prevent a repeat.
This commit is contained in:
parent
eabeeeda47
commit
5176e3e8c7
5 changed files with 91 additions and 72 deletions
|
@ -3,7 +3,7 @@ resolver = "2"
|
||||||
members = ["crates/cli", "crates/server", "crates/config", "crates/git"]
|
members = ["crates/cli", "crates/server", "crates/config", "crates/git"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
|
|
|
@ -24,8 +24,9 @@ pub struct WebhookId(String);
|
||||||
pub struct WebhookAuth(ulid::Ulid);
|
pub struct WebhookAuth(ulid::Ulid);
|
||||||
impl WebhookAuth {
|
impl WebhookAuth {
|
||||||
pub fn from_str(authorisation: &str) -> Result<Self, DecodeError> {
|
pub fn from_str(authorisation: &str) -> Result<Self, DecodeError> {
|
||||||
let id = ulid::Ulid::from_str(authorisation);
|
let id = ulid::Ulid::from_str(authorisation)?;
|
||||||
Ok(Self(id?))
|
info!("Parse auth token: {}", id);
|
||||||
|
Ok(Self(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate() -> Self {
|
fn generate() -> Self {
|
||||||
|
@ -186,23 +187,16 @@ impl Handler<WebhookMessage> for RepoActor {
|
||||||
warn!("Don't know what authorization to expect");
|
warn!("Don't know what authorization to expect");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(received_authorization) = &msg.authorisation() else {
|
if msg.authorisation() != expected_authorization {
|
||||||
warn!("Missing authorization token");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if received_authorization != expected_authorization {
|
|
||||||
warn!(
|
warn!(
|
||||||
"Invalid authorization - expected {}",
|
"Invalid authorization - expected {}",
|
||||||
expected_authorization
|
expected_authorization
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let id = msg.id();
|
|
||||||
let span = tracing::info_span!("handle", %id);
|
|
||||||
let _guard = span.enter();
|
|
||||||
let body = msg.body();
|
let body = msg.body();
|
||||||
match serde_json::from_str::<Push>(body) {
|
match serde_json::from_str::<Push>(body.as_str()) {
|
||||||
Err(err) => warn!(?err, %body, "Not a 'push'"),
|
Err(err) => warn!(?err, ?body, "Not a 'push'"),
|
||||||
Ok(push) => {
|
Ok(push) => {
|
||||||
if let Some(config) = &self.details.repo_config {
|
if let Some(config) = &self.details.repo_config {
|
||||||
match push.branch(config.branches()) {
|
match push.branch(config.branches()) {
|
||||||
|
@ -262,6 +256,10 @@ impl Handler<WebhookMessage> for RepoActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn split_ref(reference: &str) -> (&str, &str) {
|
||||||
|
reference.split_at(11)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
struct Push {
|
struct Push {
|
||||||
#[serde(rename = "ref")]
|
#[serde(rename = "ref")]
|
||||||
|
@ -298,12 +296,8 @@ impl Push {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn split_ref(reference: &str) -> (&str, &str) {
|
|
||||||
reference.split_at(11)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Branch {
|
pub enum Branch {
|
||||||
Main,
|
Main,
|
||||||
Next,
|
Next,
|
||||||
Dev,
|
Dev,
|
||||||
|
|
|
@ -1,27 +1,33 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
use git_next_config::RepoAlias;
|
||||||
|
|
||||||
use crate::actors::repo::webhook::WebhookAuth;
|
use crate::actors::repo::webhook::WebhookAuth;
|
||||||
|
|
||||||
#[derive(Message, Debug, Clone, derive_more::Constructor)]
|
#[derive(Message, Debug, Clone, derive_more::Constructor)]
|
||||||
#[rtype(result = "()")]
|
#[rtype(result = "()")]
|
||||||
pub struct WebhookMessage {
|
pub struct WebhookMessage {
|
||||||
id: String,
|
// forge // TODO: differentiate between multiple forges
|
||||||
path: String,
|
repo_alias: RepoAlias,
|
||||||
authorisation: String,
|
authorisation: WebhookAuth,
|
||||||
body: String,
|
body: Body,
|
||||||
}
|
}
|
||||||
impl WebhookMessage {
|
impl WebhookMessage {
|
||||||
pub const fn id(&self) -> &String {
|
pub const fn repo_alias(&self) -> &RepoAlias {
|
||||||
&self.id
|
&self.repo_alias
|
||||||
}
|
}
|
||||||
pub const fn path(&self) -> &String {
|
pub const fn body(&self) -> &Body {
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
pub const fn body(&self) -> &String {
|
|
||||||
&self.body
|
&self.body
|
||||||
}
|
}
|
||||||
pub fn authorisation(&self) -> Option<WebhookAuth> {
|
pub const fn authorisation(&self) -> &WebhookAuth {
|
||||||
WebhookAuth::from_str(&self.authorisation).ok()
|
&self.authorisation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, derive_more::Constructor)]
|
||||||
|
pub struct Body(String);
|
||||||
|
impl Body {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,9 +28,9 @@ impl Handler<WebhookMessage> for WebhookRouter {
|
||||||
|
|
||||||
fn handle(&mut self, msg: WebhookMessage, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: WebhookMessage, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let _gaurd = self.span.enter();
|
let _gaurd = self.span.enter();
|
||||||
let repo_alias = RepoAlias::new(msg.path());
|
let repo_alias = msg.repo_alias();
|
||||||
debug!(repo = %repo_alias, "Router...");
|
debug!(repo = %repo_alias, "Router...");
|
||||||
if let Some(recipient) = self.repos.get(&repo_alias) {
|
if let Some(recipient) = self.repos.get(repo_alias) {
|
||||||
info!(repo = %repo_alias, "Sending to Recipient");
|
info!(repo = %repo_alias, "Sending to Recipient");
|
||||||
recipient.do_send(msg);
|
recipient.do_send(msg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,17 @@ use std::net::SocketAddr;
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
|
use git_next_config::RepoAlias;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
use warp::reject::Rejection;
|
||||||
|
|
||||||
use crate::actors::webhook::message::WebhookMessage;
|
use crate::actors::{repo::webhook::WebhookAuth, webhook::message::WebhookMessage};
|
||||||
|
|
||||||
|
use super::message;
|
||||||
|
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
socket_addr: SocketAddr,
|
socket_addr: SocketAddr,
|
||||||
address: actix::prelude::Recipient<super::message::WebhookMessage>,
|
address: actix::prelude::Recipient<message::WebhookMessage>,
|
||||||
) {
|
) {
|
||||||
// start webhook server
|
// start webhook server
|
||||||
use warp::Filter;
|
use warp::Filter;
|
||||||
|
@ -21,51 +25,42 @@ pub async fn start(
|
||||||
.and(warp::body::bytes())
|
.and(warp::body::bytes())
|
||||||
.and_then(
|
.and_then(
|
||||||
|recipient: Recipient<WebhookMessage>,
|
|recipient: Recipient<WebhookMessage>,
|
||||||
path,
|
path: String,
|
||||||
// query: String,
|
// query: String,
|
||||||
headers: warp::http::HeaderMap,
|
headers: warp::http::HeaderMap,
|
||||||
body: bytes::Bytes| async move {
|
body: bytes::Bytes| async move {
|
||||||
info!("POST received");
|
info!("POST received");
|
||||||
|
let repo_alias = RepoAlias::new(path);
|
||||||
let bytes = body.to_vec();
|
let bytes = body.to_vec();
|
||||||
let request_data = String::from_utf8_lossy(&bytes).to_string();
|
let body = message::Body::new(String::from_utf8_lossy(&bytes).to_string());
|
||||||
let id = ulid::Ulid::new().to_string();
|
headers.get("Authorization").map_or_else(
|
||||||
match headers.get("Authorization") {
|
|| {
|
||||||
Some(auhorisation) => {
|
|
||||||
info!(id, path, "Received webhook");
|
|
||||||
let authorisation = auhorisation
|
|
||||||
.to_str()
|
|
||||||
.map_err(|e| {
|
|
||||||
warn!("Invalid value in authorization: {:?}", e);
|
|
||||||
warp::reject()
|
|
||||||
})? // valid characters
|
|
||||||
.strip_prefix("Basic ")
|
|
||||||
.ok_or_else(|| {
|
|
||||||
warn!("Authorization must be 'Basic'");
|
|
||||||
warp::reject()
|
|
||||||
})? // must start with "Basic "
|
|
||||||
.to_string();
|
|
||||||
let message = WebhookMessage::new(
|
|
||||||
id,
|
|
||||||
path,
|
|
||||||
/* query, headers, */ request_data,
|
|
||||||
authorisation,
|
|
||||||
);
|
|
||||||
recipient
|
|
||||||
.try_send(message)
|
|
||||||
.map(|_| {
|
|
||||||
info!("Message sent ok");
|
|
||||||
warp::reply::with_status("OK", warp::http::StatusCode::OK)
|
|
||||||
})
|
|
||||||
.map_err(|e| {
|
|
||||||
warn!("Unknown error: {:?}", e);
|
|
||||||
warp::reject()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
warn!("No Authorization header");
|
warn!("No Authorization header");
|
||||||
Err(warp::reject())
|
Err(warp::reject())
|
||||||
}
|
},
|
||||||
}
|
|authorisation_header| {
|
||||||
|
info!(?repo_alias, ?authorisation_header, "Received webhook",);
|
||||||
|
match parse_auth(authorisation_header) {
|
||||||
|
Ok(authorisation) => {
|
||||||
|
let message = WebhookMessage::new(repo_alias, authorisation, body);
|
||||||
|
recipient
|
||||||
|
.try_send(message)
|
||||||
|
.map(|_| {
|
||||||
|
info!("Message sent ok");
|
||||||
|
warp::reply::with_status("OK", warp::http::StatusCode::OK)
|
||||||
|
})
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("Unknown error: {:?}", e);
|
||||||
|
warp::reject()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(?e, "Failed to decode authorization header");
|
||||||
|
Err(warp::reject())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -73,3 +68,27 @@ pub async fn start(
|
||||||
info!("Starting webhook server: {}", socket_addr);
|
info!("Starting webhook server: {}", socket_addr);
|
||||||
warp::serve(route).run(socket_addr).await;
|
warp::serve(route).run(socket_addr).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_auth(authorization_header: &warp::http::HeaderValue) -> Result<WebhookAuth, Rejection> {
|
||||||
|
WebhookAuth::from_str(
|
||||||
|
authorization_header
|
||||||
|
.to_str()
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("Invalid non-ascii value in authorization: {:?}", e);
|
||||||
|
warp::reject()
|
||||||
|
}) // valid characters
|
||||||
|
.map(|v| {
|
||||||
|
info!("raw auth header: {}", v);
|
||||||
|
v
|
||||||
|
})?
|
||||||
|
.strip_prefix("Basic ")
|
||||||
|
.ok_or_else(|| {
|
||||||
|
warn!("Authorization must be 'Basic'");
|
||||||
|
warp::reject()
|
||||||
|
})?, // must start with "Basic "
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!(?e, "decode error");
|
||||||
|
warp::reject()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue