Skip to main content

github_bot_sdk/webhook/
mod.rs

1//! GitHub webhook processing and validation.
2//!
3//! This module provides comprehensive webhook handling for GitHub Apps, including
4//! signature validation, event processing, and async handler execution using a
5//! fire-and-forget pattern to ensure fast HTTP responses.
6//!
7//! # Core Components
8//!
9//! - [`WebhookHandler`] - Trait for application-provided event processing logic
10//! - [`WebhookReceiver`] - HTTP webhook intake with validation and async dispatch
11//! - [`SignatureValidator`] - HMAC-SHA256 signature validation
12//! - [`WebhookRequest`]/[`WebhookResponse`] - HTTP request/response types
13//!
14//! # Fire-and-Forget Pattern
15//!
16//! The receiver ensures GitHub receives responses within the 10-second timeout:
17//!
18//! 1. Validate signature (< 10ms)
19//! 2. Process/normalize event (< 5ms)
20//! 3. Return HTTP response immediately (target < 100ms)
21//! 4. Spawn async tasks for handlers (non-blocking)
22//!
23//! # Security
24//!
25//! Webhook signature validation uses HMAC-SHA256 with constant-time comparison
26//! to prevent timing attacks. All validation operations complete in under 100ms.
27//!
28//! # Complete Usage Example
29//!
30//! ## Basic Webhook Handler
31//!
32//! ```rust,no_run
33//! use github_bot_sdk::webhook::{WebhookHandler, WebhookReceiver, WebhookRequest};
34//! use github_bot_sdk::auth::SecretProvider;
35//! use github_bot_sdk::events::{EventProcessor, ProcessorConfig, EventEnvelope};
36//! use async_trait::async_trait;
37//! use std::sync::Arc;
38//! use std::collections::HashMap;
39//!
40//! // Define your handler
41//! struct MyBotHandler;
42//!
43//! #[async_trait]
44//! impl WebhookHandler for MyBotHandler {
45//!     async fn handle_event(
46//!         &self,
47//!         envelope: &EventEnvelope,
48//!     ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
49//!         match envelope.event_type.as_str() {
50//!             "pull_request" => {
51//!                 println!("Processing PR event: {}", envelope.event_id);
52//!                 // Add your PR processing logic here
53//!             }
54//!             "issues" => {
55//!                 println!("Processing issue event: {}", envelope.event_id);
56//!                 // Add your issue processing logic here
57//!             }
58//!             _ => {
59//!                 println!("Ignoring event type: {}", envelope.event_type);
60//!             }
61//!         }
62//!         Ok(())
63//!     }
64//! }
65//!
66//! # async fn example(secret_provider: Arc<dyn SecretProvider>) -> Result<(), Box<dyn std::error::Error>> {
67//! // Setup receiver with processor
68//! let processor = EventProcessor::new(ProcessorConfig::default());
69//! let mut receiver = WebhookReceiver::new(secret_provider, processor);
70//!
71//! // Register your handler
72//! receiver.add_handler(Arc::new(MyBotHandler)).await;
73//!
74//! // Process incoming webhook (typically called from HTTP server)
75//! let headers = HashMap::from([
76//!     ("x-github-event".to_string(), "pull_request".to_string()),
77//!     ("x-github-delivery".to_string(), "12345-67890".to_string()),
78//!     ("x-hub-signature-256".to_string(), "sha256=abc...".to_string()),
79//! ]);
80//! let body = bytes::Bytes::from_static(b"{\"action\":\"opened\",\"number\":1}");
81//! let request = WebhookRequest::new(headers, body);
82//!
83//! let response = receiver.receive_webhook(request).await;
84//! println!("Response status: {}", response.status_code());
85//! # Ok(())
86//! # }
87//! ```
88//!
89//! ## Multiple Handlers
90//!
91//! ```rust,no_run
92//! use github_bot_sdk::webhook::{WebhookHandler, WebhookReceiver};
93//! use github_bot_sdk::events::EventEnvelope;
94//! use async_trait::async_trait;
95//! use std::sync::Arc;
96//!
97//! // Define specialized handlers
98//! struct PullRequestHandler;
99//! struct IssueHandler;
100//! struct SecurityHandler;
101//!
102//! #[async_trait]
103//! impl WebhookHandler for PullRequestHandler {
104//!     async fn handle_event(&self, envelope: &EventEnvelope) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
105//!         if envelope.event_type == "pull_request" {
106//!             println!("PR Handler: Processing {}", envelope.event_id);
107//!             // PR-specific logic
108//!         }
109//!         Ok(())
110//!     }
111//! }
112//!
113//! #[async_trait]
114//! impl WebhookHandler for IssueHandler {
115//!     async fn handle_event(&self, envelope: &EventEnvelope) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
116//!         if envelope.event_type == "issues" {
117//!             println!("Issue Handler: Processing {}", envelope.event_id);
118//!             // Issue-specific logic
119//!         }
120//!         Ok(())
121//!     }
122//! }
123//!
124//! #[async_trait]
125//! impl WebhookHandler for SecurityHandler {
126//!     async fn handle_event(&self, envelope: &EventEnvelope) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
127//!         // Security monitoring across all event types
128//!         println!("Security: Auditing event {}", envelope.event_id);
129//!         Ok(())
130//!     }
131//! }
132//!
133//! # async fn example(mut receiver: WebhookReceiver) -> Result<(), Box<dyn std::error::Error>> {
134//! // Register multiple handlers - all will be invoked concurrently
135//! receiver.add_handler(Arc::new(PullRequestHandler)).await;
136//! receiver.add_handler(Arc::new(IssueHandler)).await;
137//! receiver.add_handler(Arc::new(SecurityHandler)).await;
138//! # Ok(())
139//! # }
140//! ```
141//!
142//! ## HTTP Server Integration (Axum Example)
143//!
144//! ```rust,ignore
145//! use github_bot_sdk::webhook::{WebhookReceiver, WebhookRequest, WebhookResponse};
146//! use axum::{
147//!     extract::State,
148//!     http::{HeaderMap, StatusCode},
149//!     response::{IntoResponse, Response},
150//!     Json, Router,
151//!     routing::post,
152//! };
153//! use bytes::Bytes;
154//! use std::sync::Arc;
155//!
156//! // Application state with receiver
157//! #[derive(Clone)]
158//! struct AppState {
159//!     receiver: Arc<WebhookReceiver>,
160//! }
161//!
162//! // HTTP handler for webhook endpoint
163//! async fn handle_webhook(
164//!     State(state): State<AppState>,
165//!     headers: HeaderMap,
166//!     body: Bytes,
167//! ) -> Response {
168//!     // Convert HTTP headers to HashMap
169//!     let header_map: std::collections::HashMap<String, String> = headers
170//!         .iter()
171//!         .map(|(k, v)| (k.as_str().to_lowercase(), v.to_str().unwrap_or("").to_string()))
172//!         .collect();
173//!
174//!     // Create webhook request
175//!     let request = WebhookRequest::new(header_map, body);
176//!
177//!     // Process webhook
178//!     let response = state.receiver.receive_webhook(request).await;
179//!
180//!     // Convert to HTTP response
181//!     let status = StatusCode::from_u16(response.status_code()).unwrap();
182//!     let message = response.message().to_string();
183//!
184//!     (status, Json(serde_json::json!({
185//!         "message": message
186//!     }))).into_response()
187//! }
188//!
189//! # async fn example(receiver: Arc<WebhookReceiver>) -> Result<(), Box<dyn std::error::Error>> {
190//! let state = AppState { receiver };
191//!
192//! let app = Router::new()
193//!     .route("/webhook", post(handle_webhook))
194//!     .with_state(state);
195//!
196//! // Run server
197//! // let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
198//! // axum::serve(listener, app).await?;
199//! # Ok(())
200//! # }
201//! ```
202//!
203//! ## Direct Signature Validation
204//!
205//! If you need to validate signatures independently:
206//!
207//! ```rust,no_run
208//! use github_bot_sdk::webhook::SignatureValidator;
209//! use github_bot_sdk::auth::SecretProvider;
210//! use std::sync::Arc;
211//!
212//! # async fn example(secret_provider: Arc<dyn SecretProvider>) -> Result<(), Box<dyn std::error::Error>> {
213//! let validator = SignatureValidator::new(secret_provider);
214//!
215//! let payload = b"{\"action\":\"opened\",\"number\":1}";
216//! let signature = "sha256=5c4a...";  // From X-Hub-Signature-256 header
217//!
218//! let is_valid = validator.validate(payload, signature).await?;
219//! if is_valid {
220//!     println!("Webhook signature is valid");
221//! } else {
222//!     println!("Invalid webhook signature - possible tampering");
223//! }
224//! # Ok(())
225//! # }
226//! ```
227
228pub mod handler;
229pub mod receiver;
230pub mod validation;
231
232// Re-export main types
233pub use handler::WebhookHandler;
234pub use receiver::{WebhookReceiver, WebhookRequest, WebhookResponse};
235pub use validation::SignatureValidator;
236
237#[cfg(test)]
238#[path = "mod_tests.rs"]
239mod tests;