1use hmac::{Hmac, Mac};
2use sha2::Sha256;
3use serde::{Deserialize, Serialize};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6type HmacSha256 = Hmac<Sha256>;
7
8pub struct Webhook;
9
10impl Webhook {
11 pub fn verify_signature(payload: &str, header: Option<&str>, secret: &str) -> Result<bool, &'static str> {
12 Self::verify_signature_with_tolerance(payload, header, secret, 300)
13 }
14
15 pub fn verify_signature_with_tolerance(payload: &str, header: Option<&str>, secret: &str, tolerance: u64) -> Result<bool, &'static str> {
16 if secret.is_empty() { return Err("Webhook secret is required"); }
17
18 let header = match header {
19 Some(h) if !h.is_empty() => h,
20 _ => return Ok(false),
21 };
22
23 let mut timestamp: Option<&str> = None;
24 let mut signature: Option<&str> = None;
25
26 for part in header.split(',') {
27 let kv: Vec<&str> = part.splitn(2, '=').collect();
28 if kv.len() == 2 {
29 match kv[0].trim() {
30 "t" => timestamp = Some(kv[1].trim()),
31 "v1" => signature = Some(kv[1].trim()),
32 _ => {}
33 }
34 }
35 }
36
37 let (ts, sig) = match (timestamp, signature) {
38 (Some(t), Some(s)) => (t, s),
39 _ => return Ok(false),
40 };
41
42 let ts_num: u64 = ts.parse().map_err(|_| "Invalid timestamp")?;
43 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
44 if now.abs_diff(ts_num) > tolerance { return Ok(false); }
45
46 let message = format!("{}.{}", ts, payload);
47 let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
48 mac.update(message.as_bytes());
49 let result = mac.finalize();
50 let expected: String = result.into_bytes().iter().map(|b| format!("{:02x}", b)).collect();
51
52 Ok(expected == sig)
53 }
54
55 pub fn parse(payload: &str) -> Result<WebhookEvent, serde_json::Error> {
56 serde_json::from_str(payload)
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct WebhookEvent {
62 #[serde(rename = "requestId")]
63 pub request_id: Option<String>,
64 #[serde(rename = "sessionId")]
65 pub session_id: Option<String>,
66 #[serde(rename = "userId")]
67 pub user_id: Option<String>,
68 pub status: Option<String>,
69 pub content: Option<Vec<ContentItem>>,
70 pub error: Option<ErrorInfo>,
71 pub clarification: Option<ClarificationInfo>,
72 pub timestamp: Option<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ContentItem {
77 #[serde(rename = "type")]
78 pub content_type: Option<String>,
79 pub text: Option<String>,
80 pub url: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ErrorInfo {
85 pub code: Option<String>,
86 pub message: Option<String>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ClarificationInfo {
91 pub question: Option<String>,
92 pub options: Option<Vec<String>>,
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_verify_signature_missing_secret() {
101 assert!(Webhook::verify_signature("payload", Some("t=123,v1=abc"), "").is_err());
102 }
103
104 #[test]
105 fn test_verify_signature_null_header() {
106 assert_eq!(Webhook::verify_signature("payload", None, "secret").unwrap(), false);
107 }
108
109 #[test]
110 fn test_verify_signature_empty_header() {
111 assert_eq!(Webhook::verify_signature("payload", Some(""), "secret").unwrap(), false);
112 }
113
114 #[test]
115 fn test_parse_completed_payload() {
116 let payload = r#"{"status":"completed","content":[{"type":"text","text":"Hello"}]}"#;
117 let event = Webhook::parse(payload).unwrap();
118 assert_eq!(event.status, Some("completed".to_string()));
119 assert!(event.content.is_some());
120 }
121
122 #[test]
123 fn test_parse_failed_payload() {
124 let payload = r#"{"status":"failed","error":{"code":"ERROR","message":"Something went wrong"}}"#;
125 let event = Webhook::parse(payload).unwrap();
126 assert_eq!(event.status, Some("failed".to_string()));
127 assert!(event.error.is_some());
128 }
129}