use crate::auth::SecretProvider;
use crate::events::EventProcessor;
use crate::webhook::handler::WebhookHandler;
use crate::webhook::validation::SignatureValidator;
use bytes::Bytes;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info, warn};
#[derive(Debug, Clone)]
pub struct WebhookRequest {
headers: HashMap<String, String>,
body: Bytes,
}
impl WebhookRequest {
pub fn new(headers: HashMap<String, String>, body: Bytes) -> Self {
Self { headers, body }
}
pub fn event_type(&self) -> Option<&str> {
self.headers
.get("x-github-event")
.or_else(|| self.headers.get("X-GitHub-Event"))
.map(|s| s.as_str())
}
pub fn delivery_id(&self) -> Option<&str> {
self.headers
.get("x-github-delivery")
.or_else(|| self.headers.get("X-GitHub-Delivery"))
.map(|s| s.as_str())
}
pub fn signature(&self) -> Option<&str> {
self.headers
.get("x-hub-signature-256")
.or_else(|| self.headers.get("X-Hub-Signature-256"))
.map(|s| s.as_str())
}
pub fn payload(&self) -> &[u8] {
&self.body
}
pub fn headers(&self) -> &HashMap<String, String> {
&self.headers
}
}
#[derive(Debug, Clone)]
pub enum WebhookResponse {
Ok { message: String, event_id: String },
Unauthorized { message: String },
BadRequest { message: String },
InternalError { message: String },
}
impl WebhookResponse {
pub fn status_code(&self) -> u16 {
match self {
Self::Ok { .. } => 200,
Self::Unauthorized { .. } => 401,
Self::BadRequest { .. } => 400,
Self::InternalError { .. } => 500,
}
}
pub fn message(&self) -> &str {
match self {
Self::Ok { message, .. } => message,
Self::Unauthorized { message } => message,
Self::BadRequest { message } => message,
Self::InternalError { message } => message,
}
}
pub fn is_success(&self) -> bool {
matches!(self, Self::Ok { .. })
}
}
pub struct WebhookReceiver {
validator: SignatureValidator,
processor: EventProcessor,
handlers: Arc<RwLock<Vec<Arc<dyn WebhookHandler>>>>,
}
impl WebhookReceiver {
pub fn new(secrets: Arc<dyn SecretProvider>, processor: EventProcessor) -> Self {
let validator = SignatureValidator::new(secrets);
Self {
validator,
processor,
handlers: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn add_handler(&mut self, handler: Arc<dyn WebhookHandler>) {
let mut handlers = self.handlers.write().await;
handlers.push(handler);
}
pub async fn receive_webhook(&self, request: WebhookRequest) -> WebhookResponse {
let event_type = match request.event_type() {
Some(et) => et,
None => {
return WebhookResponse::BadRequest {
message: "Missing X-GitHub-Event header".to_string(),
};
}
};
let signature = match request.signature() {
Some(sig) => sig,
None => {
return WebhookResponse::Unauthorized {
message: "Missing X-Hub-Signature-256 header".to_string(),
};
}
};
let delivery_id = request.delivery_id();
match self.validator.validate(request.payload(), signature).await {
Ok(true) => {
info!(
event_type = %event_type,
delivery_id = ?delivery_id,
"Webhook signature validated"
);
}
Ok(false) => {
warn!(
event_type = %event_type,
delivery_id = ?delivery_id,
"Invalid webhook signature"
);
return WebhookResponse::Unauthorized {
message: "Invalid signature".to_string(),
};
}
Err(e) => {
error!(
event_type = %event_type,
delivery_id = ?delivery_id,
error = %e,
"Signature validation failed"
);
return WebhookResponse::InternalError {
message: format!("Validation error: {}", e),
};
}
}
let envelope = match self
.processor
.process_webhook(event_type, request.payload(), delivery_id)
.await
{
Ok(env) => env,
Err(e) => {
error!(
event_type = %event_type,
delivery_id = ?delivery_id,
error = %e,
"Event processing failed"
);
return WebhookResponse::BadRequest {
message: format!("Invalid webhook payload: {}", e),
};
}
};
let event_id = envelope.event_id.to_string();
info!(
event_id = %envelope.event_id,
event_type = %envelope.event_type,
repository = %envelope.repository.full_name,
"Webhook processed successfully"
);
let handlers = self.handlers.clone();
tokio::spawn(async move {
let handlers_guard = handlers.read().await;
for handler in handlers_guard.iter() {
let handler_clone = handler.clone();
let envelope_clone = envelope.clone();
tokio::spawn(async move {
if let Err(e) = handler_clone.handle_event(&envelope_clone).await {
error!(
event_id = %envelope_clone.event_id,
error = %e,
"Handler execution failed"
);
}
});
}
});
WebhookResponse::Ok {
message: "Webhook received".to_string(),
event_id,
}
}
}
#[cfg(test)]
#[path = "receiver_tests.rs"]
mod tests;