use crate::error::{DrivenError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default)]
pub auth: WebhookAuth,
#[serde(default)]
pub endpoints: HashMap<String, WebhookEndpoint>,
#[serde(default)]
pub security: WebhookSecurity,
}
fn default_true() -> bool {
true
}
fn default_port() -> u16 {
8792
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum WebhookAuth {
#[default]
None,
Bearer,
Hmac,
Basic,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookEndpoint {
pub path: String,
#[serde(default)]
pub secret: String,
pub handler: Option<String>,
#[serde(default = "default_true")]
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookSecurity {
#[serde(default = "default_allowed_ips")]
pub allowed_ips: Vec<String>,
#[serde(default = "default_rate_limit")]
pub rate_limit: u32,
#[serde(default = "default_max_body_size")]
pub max_body_size: usize,
}
fn default_allowed_ips() -> Vec<String> {
vec!["0.0.0.0/0".to_string()]
}
fn default_rate_limit() -> u32 {
100
}
fn default_max_body_size() -> usize {
1024 * 1024 }
impl Default for WebhookSecurity {
fn default() -> Self {
Self {
allowed_ips: default_allowed_ips(),
rate_limit: default_rate_limit(),
max_body_size: default_max_body_size(),
}
}
}
impl Default for WebhookConfig {
fn default() -> Self {
Self {
enabled: true,
port: default_port(),
auth: WebhookAuth::default(),
endpoints: HashMap::new(),
security: WebhookSecurity::default(),
}
}
}
impl WebhookConfig {
pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| DrivenError::Io(e))?;
Self::parse_sr(&content)
}
fn parse_sr(_content: &str) -> Result<Self> {
Ok(Self::default())
}
pub fn resolve_env_vars(&mut self) {
for endpoint in self.endpoints.values_mut() {
if endpoint.secret.starts_with('$') {
let var_name = &endpoint.secret[1..];
endpoint.secret = std::env::var(var_name).unwrap_or_default();
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookEvent {
pub endpoint: String,
pub method: String,
pub headers: HashMap<String, String>,
pub body: String,
pub json: Option<serde_json::Value>,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub source_ip: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookResponse {
pub status: u16,
pub body: String,
pub headers: HashMap<String, String>,
}
impl Default for WebhookResponse {
fn default() -> Self {
Self {
status: 200,
body: "OK".to_string(),
headers: HashMap::new(),
}
}
}
pub type WebhookHandler = Box<dyn Fn(WebhookEvent) -> WebhookResponse + Send + Sync>;
pub struct WebhookServer {
config: WebhookConfig,
handlers: Arc<RwLock<HashMap<String, WebhookHandler>>>,
events: Arc<RwLock<Vec<WebhookEvent>>>,
}
impl WebhookServer {
pub fn new(config: &WebhookConfig) -> Result<Self> {
let mut config = config.clone();
config.resolve_env_vars();
Ok(Self {
config,
handlers: Arc::new(RwLock::new(HashMap::new())),
events: Arc::new(RwLock::new(Vec::new())),
})
}
pub async fn register_handler(&self, endpoint: &str, handler: WebhookHandler) {
let mut handlers = self.handlers.write().await;
handlers.insert(endpoint.to_string(), handler);
}
pub async fn start(&self) -> Result<()> {
if !self.config.enabled {
return Err(DrivenError::Config("Webhooks are disabled".into()));
}
let port = self.config.port;
tracing::info!("Starting webhook server on port {}", port);
self.run_server(port).await
}
pub async fn stop(&self) -> Result<()> {
tracing::info!("Stopping webhook server");
Ok(())
}
pub async fn get_events(&self, limit: usize) -> Vec<WebhookEvent> {
let events = self.events.read().await;
events.iter().rev().take(limit).cloned().collect()
}
pub async fn process_webhook(&self, endpoint: &str, event: WebhookEvent) -> Result<WebhookResponse> {
let endpoint_config = self.config.endpoints.get(endpoint)
.ok_or_else(|| DrivenError::NotFound(format!("Endpoint '{}' not found", endpoint)))?;
if !endpoint_config.enabled {
return Err(DrivenError::Config("Endpoint is disabled".into()));
}
if !endpoint_config.secret.is_empty() {
self.verify_signature(&event, &endpoint_config.secret)?;
}
{
let mut events = self.events.write().await;
events.push(event.clone());
if events.len() > 1000 {
events.drain(0..events.len() - 1000);
}
}
let handlers = self.handlers.read().await;
if let Some(handler) = handlers.get(endpoint) {
Ok(handler(event))
} else {
Ok(WebhookResponse::default())
}
}
fn verify_signature(&self, event: &WebhookEvent, secret: &str) -> Result<()> {
if let Some(signature) = event.headers.get("x-hub-signature-256") {
return self.verify_github_signature(&event.body, signature, secret);
}
if let Some(signature) = event.headers.get("stripe-signature") {
return self.verify_stripe_signature(&event.body, signature, secret);
}
if let Some(signature) = event.headers.get("x-signature") {
return self.verify_hmac_signature(&event.body, signature, secret);
}
Ok(())
}
fn verify_github_signature(&self, body: &str, signature: &str, secret: &str) -> Result<()> {
use hmac::{Hmac, Mac};
use sha2::Sha256;
let signature = signature.strip_prefix("sha256=").unwrap_or(signature);
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
.map_err(|_| DrivenError::Security("Invalid secret".into()))?;
mac.update(body.as_bytes());
let expected = hex::encode(mac.finalize().into_bytes());
if expected != signature {
return Err(DrivenError::Security("Invalid signature".into()));
}
Ok(())
}
fn verify_stripe_signature(&self, _body: &str, _signature: &str, _secret: &str) -> Result<()> {
Ok(())
}
fn verify_hmac_signature(&self, body: &str, signature: &str, secret: &str) -> Result<()> {
self.verify_github_signature(body, signature, secret)
}
async fn run_server(&self, port: u16) -> Result<()> {
tracing::info!("Webhook server listening on 0.0.0.0:{}", port);
tokio::signal::ctrl_c()
.await
.map_err(|e| DrivenError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
Ok(())
}
}
pub async fn test_webhook(config: &WebhookConfig, endpoint: &str, payload: &str) -> Result<WebhookResponse> {
let server = WebhookServer::new(config)?;
let event = WebhookEvent {
endpoint: endpoint.to_string(),
method: "POST".to_string(),
headers: HashMap::new(),
body: payload.to_string(),
json: serde_json::from_str(payload).ok(),
timestamp: chrono::Utc::now(),
source_ip: "127.0.0.1".to_string(),
};
server.process_webhook(endpoint, event).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = WebhookConfig::default();
assert!(config.enabled);
assert_eq!(config.port, 8792);
}
#[test]
fn test_default_security() {
let security = WebhookSecurity::default();
assert_eq!(security.rate_limit, 100);
assert_eq!(security.max_body_size, 1024 * 1024);
}
}