use axum::{ extract::Request, http::{HeaderMap, StatusCode}, response::Response, routing::post, Router, }; use chrono::Utc; use hmac::{Hmac, Mac}; use sha2::Sha256; use std::collections::HashMap; use tokio::net::TcpListener; const SERVER_PORT: u16 = 5080; const MAX_WEBHOOK_AGE: i64 = 120 * 1000; // 2 minutes in milliseconds const OPENVIDU_MEET_API_KEY: &str = "meet-api-key"; type HmacSha256 = Hmac; #[tokio::main] async fn main() { println!("Webhook server listening on port {}", SERVER_PORT); let app = Router::new().route("/webhook", post(webhook_handler)); let listener = TcpListener::bind(format!("0.0.0.0:{}", SERVER_PORT)) .await .unwrap(); axum::serve(listener, app).await.unwrap(); } async fn webhook_handler( headers: HeaderMap, request: Request, ) -> Result, StatusCode> { let body = match axum::body::to_bytes(request.into_body(), usize::MAX).await { Ok(bytes) => bytes, Err(_) => return Err(StatusCode::BAD_REQUEST), }; let body_str = match std::str::from_utf8(&body) { Ok(s) => s, Err(_) => return Err(StatusCode::BAD_REQUEST), }; // Extract headers let mut header_map = HashMap::new(); for (key, value) in headers.iter() { if let Ok(value_str) = value.to_str() { header_map.insert(key.as_str().to_lowercase(), value_str.to_string()); } } if !is_webhook_event_valid(body_str, &header_map) { println!("Invalid webhook signature"); return Ok(Response::builder() .status(StatusCode::UNAUTHORIZED) .body("Invalid webhook signature".to_string()) .unwrap()); } println!("Webhook received: {}", body_str); Ok(Response::builder() .status(StatusCode::OK) .body("".to_string()) .unwrap()) } fn is_webhook_event_valid(body_str: &str, headers: &HashMap) -> bool { let signature = match headers.get("x-signature") { Some(sig) => sig, None => return false, }; let timestamp_str = match headers.get("x-timestamp") { Some(ts) => ts, None => return false, }; let timestamp: i64 = match timestamp_str.parse() { Ok(ts) => ts, Err(_) => return false, }; // Check timestamp age let current = Utc::now().timestamp_millis(); let diff_time = current - timestamp; if diff_time >= MAX_WEBHOOK_AGE { return false; } // Create signed payload using the raw body string let signed_payload = format!("{}.{}", timestamp, body_str); // Calculate HMAC let mut mac = match HmacSha256::new_from_slice(OPENVIDU_MEET_API_KEY.as_bytes()) { Ok(mac) => mac, Err(_) => return false, }; mac.update(signed_payload.as_bytes()); let expected = mac.finalize().into_bytes(); let expected_hex = hex::encode(expected); // Timing-safe comparison if signature.len() != expected_hex.len() { return false; } let mut result = 0u8; for (a, b) in signature.bytes().zip(expected_hex.bytes()) { result |= a ^ b; } result == 0 }