systemprompt_agent/services/external_integrations/webhook/service/
mod.rs1mod delivery;
13mod types;
14
15pub use types::{RetryPolicy, WebhookConfig, WebhookDeliveryResult, WebhookTestResult};
16
17use hmac::{Hmac, KeyInit, Mac};
18use reqwest::Client;
19use serde_json::Value;
20use sha2::Sha256;
21use std::collections::HashMap;
22use systemprompt_identifiers::WebhookEndpointId;
23use tokio::sync::RwLock;
24
25use crate::models::external_integrations::{
26 IntegrationError, IntegrationResult, WebhookEndpoint, WebhookRequest, WebhookResponse,
27};
28
29type HmacSha256 = Hmac<Sha256>;
30
31#[derive(Debug)]
32pub struct WebhookService {
33 pub(super) endpoints: RwLock<HashMap<WebhookEndpointId, WebhookEndpoint>>,
34 pub(super) http_client: Client,
35}
36
37impl WebhookService {
38 pub fn new() -> Self {
39 let http_client = Client::builder()
40 .timeout(std::time::Duration::from_secs(10))
41 .connect_timeout(std::time::Duration::from_secs(5))
42 .build()
43 .unwrap_or_else(|_| Client::new());
44 Self {
45 endpoints: RwLock::new(HashMap::new()),
46 http_client,
47 }
48 }
49
50 pub async fn register_endpoint(
51 &self,
52 mut endpoint: WebhookEndpoint,
53 ) -> IntegrationResult<WebhookEndpointId> {
54 if endpoint.id.as_str().is_empty() {
55 endpoint.id = WebhookEndpointId::generate();
56 }
57
58 let endpoint_id = endpoint.id.clone();
59
60 {
61 let mut endpoints = self.endpoints.write().await;
62 endpoints.insert(endpoint_id.clone(), endpoint);
63 }
64
65 Ok(endpoint_id)
66 }
67
68 pub async fn update_endpoint(&self, endpoint: WebhookEndpoint) -> IntegrationResult<()> {
69 {
70 let mut endpoints = self.endpoints.write().await;
71 endpoints.insert(endpoint.id.clone(), endpoint);
72 }
73 Ok(())
74 }
75
76 pub async fn get_endpoint(
77 &self,
78 endpoint_id: &WebhookEndpointId,
79 ) -> IntegrationResult<Option<WebhookEndpoint>> {
80 let endpoints = self.endpoints.read().await;
81 Ok(endpoints.get(endpoint_id).cloned())
82 }
83
84 pub async fn list_endpoints(&self) -> IntegrationResult<Vec<WebhookEndpoint>> {
85 let endpoints = self.endpoints.read().await;
86 Ok(endpoints.values().cloned().collect())
87 }
88
89 pub async fn remove_endpoint(
90 &self,
91 endpoint_id: &WebhookEndpointId,
92 ) -> IntegrationResult<bool> {
93 let mut endpoints = self.endpoints.write().await;
94 Ok(endpoints.remove(endpoint_id).is_some())
95 }
96
97 pub async fn handle_webhook(
98 &self,
99 endpoint_id: &WebhookEndpointId,
100 request: WebhookRequest,
101 ) -> IntegrationResult<WebhookResponse> {
102 let endpoint = {
103 let endpoints = self.endpoints.read().await;
104 endpoints.get(endpoint_id).cloned().ok_or_else(|| {
105 IntegrationError::Webhook(format!("Endpoint not found: {endpoint_id}"))
106 })?
107 };
108
109 if !endpoint.active {
110 return Ok(WebhookResponse {
111 status: 404,
112 body: Some(serde_json::json!({"error": "Endpoint is inactive"})),
113 });
114 }
115
116 if let (Some(_secret), Some(signature)) = (&endpoint.secret, &request.signature)
117 && !Self::verify_signature_internal(&endpoint, &request.body, signature)?
118 {
119 return Ok(WebhookResponse {
120 status: 401,
121 body: Some(serde_json::json!({"error": "Invalid signature"})),
122 });
123 }
124
125 let event_type = request
126 .headers
127 .get("x-webhook-event")
128 .or_else(|| request.headers.get("x-event-type"))
129 .or_else(|| request.headers.get("x-github-event"))
130 .cloned()
131 .unwrap_or_else(|| "unknown".to_owned());
132
133 if !endpoint.events.is_empty()
134 && !endpoint.events.contains(&event_type)
135 && !endpoint.events.contains(&"*".to_owned())
136 {
137 return Ok(WebhookResponse {
138 status: 200,
139 body: Some(serde_json::json!({"message": "Event type not subscribed"})),
140 });
141 }
142
143 Ok(WebhookResponse {
144 status: 200,
145 body: Some(serde_json::json!({
146 "message": "Webhook processed successfully",
147 "event_type": event_type,
148 "endpoint_id": endpoint_id
149 })),
150 })
151 }
152
153 pub async fn verify_signature(
154 &self,
155 endpoint_id: &WebhookEndpointId,
156 payload: &Value,
157 signature: &str,
158 ) -> IntegrationResult<bool> {
159 let endpoint = {
160 let endpoints = self.endpoints.read().await;
161 endpoints.get(endpoint_id).cloned().ok_or_else(|| {
162 IntegrationError::Webhook(format!("Endpoint not found: {endpoint_id}"))
163 })?
164 };
165
166 Self::verify_signature_internal(&endpoint, payload, signature)
167 }
168
169 pub(super) fn verify_signature_internal(
170 endpoint: &WebhookEndpoint,
171 payload: &Value,
172 signature: &str,
173 ) -> IntegrationResult<bool> {
174 let secret = endpoint.secret.as_ref().ok_or_else(|| {
175 IntegrationError::Webhook("No secret configured for endpoint".to_owned())
176 })?;
177
178 let expected_signature = Self::generate_signature(secret, payload)?;
179
180 Ok(Self::secure_compare(&expected_signature, signature))
181 }
182
183 pub(super) fn generate_signature(secret: &str, payload: &Value) -> IntegrationResult<String> {
184 let payload_bytes = serde_json::to_vec(payload)?;
185
186 let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
187 .map_err(|e| IntegrationError::Webhook(format!("Invalid secret: {e}")))?;
188
189 mac.update(&payload_bytes);
190 let result = mac.finalize();
191 let hex_result = hex::encode(result.into_bytes());
192
193 Ok(format!("sha256={hex_result}"))
194 }
195
196 fn secure_compare(a: &str, b: &str) -> bool {
197 if a.len() != b.len() {
198 return false;
199 }
200
201 let mut result = 0u8;
202 for (byte_a, byte_b) in a.bytes().zip(b.bytes()) {
203 result |= byte_a ^ byte_b;
204 }
205
206 result == 0
207 }
208}
209
210impl Default for WebhookService {
211 fn default() -> Self {
212 Self::new()
213 }
214}