gitdataai/libs/service/webhook_dispatch.rs
2026-04-14 19:02:01 +08:00

141 lines
3.4 KiB
Rust

use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::time::Duration;
type HmacSha256 = Hmac<Sha256>;
/// 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<String> {
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::<String>()
))
}
/// 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<CommitPayload<'a>>,
}
/// 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<String>,
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),
}