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