use hmac::{Hmac, Mac}; use sha2::Sha256; use std::time::Duration; type HmacSha256 = Hmac; /// Signs a payload body using HMAC-SHA256 with the given secret. /// Returns the "X-Hub-Signature-256" header value. pub fn sign_payload(body: &[u8], secret: &str) -> Option { if secret.is_empty() { return None; } let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).ok()?; mac.update(body); let bytes = mac.finalize().into_bytes(); Some(format!( "sha256={}", bytes.iter().map(|b| format!("{:02x}", b)).collect::() )) } /// Payload sent for a push event webhook. #[derive(Debug, serde::Serialize)] pub struct PushPayload<'a> { #[serde(rename = "ref")] pub r#ref: &'a str, pub before: &'a str, pub after: &'a str, pub repository: RepositoryPayload<'a>, pub pusher: PusherPayload<'a>, #[serde(skip_serializing_if = "Vec::is_empty")] pub commits: Vec>, } /// Payload sent for a tag push event webhook. #[derive(Debug, serde::Serialize)] pub struct TagPushPayload<'a> { #[serde(rename = "ref")] pub r#ref: &'a str, pub before: &'a str, pub after: &'a str, pub repository: RepositoryPayload<'a>, pub pusher: PusherPayload<'a>, } #[derive(Debug, serde::Serialize)] pub struct RepositoryPayload<'a> { pub id: i64, pub name: &'a str, pub full_name: &'a str, pub namespace: &'a str, pub default_branch: &'a str, } #[derive(Debug, serde::Serialize)] pub struct PusherPayload<'a> { pub name: &'a str, pub email: &'a str, } #[derive(Debug, serde::Serialize)] pub struct CommitPayload<'a> { pub id: &'a str, pub message: &'a str, pub author: AuthorPayload<'a>, } #[derive(Debug, serde::Serialize)] pub struct AuthorPayload<'a> { pub name: &'a str, pub email: &'a str, } /// A configured webhook destination. #[derive(Debug, Clone)] pub struct WebhookTarget { pub id: i64, pub url: String, pub secret: Option, pub content_type: String, pub events: WebhookEvents, pub active: bool, } #[derive(Debug, Default, Clone)] pub struct WebhookEvents { pub push: bool, pub tag_push: bool, pub pull_request: bool, pub issue_comment: bool, pub release: bool, } /// Dispatches a webhook HTTP POST request. pub async fn deliver( client: &reqwest::Client, url: &str, secret: Option<&str>, content_type: &str, body: &[u8], ) -> Result<(), DispatchError> { let mut req = client .post(url) .header("Content-Type", content_type) .header("User-Agent", "Code-Git-Hook/1.0") .header("X-Webhook-Event", "push") .timeout(Duration::from_secs(10)) .body(body.to_vec()); if let Some(secret) = secret { if let Some(sig) = sign_payload(body, secret) { req = req.header("X-Hub-Signature-256", sig); } } let resp = req.send().await.map_err(|e| { if e.is_timeout() { DispatchError::Timeout } else if e.is_connect() { DispatchError::ConnectionFailed } else { DispatchError::RequestFailed(e.to_string()) } })?; let status = resp.status(); if status.is_success() { Ok(()) } else { Err(DispatchError::HttpError(status.as_u16())) } } #[derive(Debug)] pub enum DispatchError { Timeout, ConnectionFailed, RequestFailed(String), HttpError(u16), }