141 lines
3.4 KiB
Rust
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),
|
|
}
|