use super::render::{self, PdAction};
use super::spec::{PagerdutyConfig, SlackConfig, WebhookConfig};
use crate::notify::event::NotifyEvent;
use hmac::{Hmac, Mac};
use reqwest::Client;
use serde_json::Value;
use sha2::Sha256;
pub const PAGERDUTY_ENDPOINT: &str = "https://events.pagerduty.com/v2/enqueue";
pub async fn send_slack(
client: &Client,
cfg: &SlackConfig,
event: &NotifyEvent,
) -> Result<(), String> {
let body = render::slack(cfg, event);
post_json(client, &cfg.webhook_url, &body, &[]).await
}
pub async fn send_pagerduty(
client: &Client,
cfg: &PagerdutyConfig,
event: &NotifyEvent,
action: PdAction,
dedup_key: &str,
) -> Result<(), String> {
let body = render::pagerduty(cfg, event, action, dedup_key);
let url = cfg.endpoint.as_deref().unwrap_or(PAGERDUTY_ENDPOINT);
post_json(client, url, &body, &[]).await
}
pub async fn send_webhook(
client: &Client,
cfg: &WebhookConfig,
event: &NotifyEvent,
) -> Result<(), String> {
let body = render::webhook(cfg, event);
let raw = serde_json::to_vec(&body).map_err(|e| format!("serializing webhook body: {e}"))?;
let mut headers: Vec<(String, String)> = cfg
.headers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if let Some(secret) = &cfg.hmac_secret {
headers.push((
cfg.signature_header.clone(),
hmac_sha256_hex(secret.as_bytes(), &raw),
));
}
let method = reqwest::Method::from_bytes(cfg.method.as_bytes())
.map_err(|_| format!("invalid HTTP method `{}`", cfg.method))?;
let mut req = client
.request(method, &cfg.url)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(raw);
for (k, v) in &headers {
req = req.header(k, v);
}
let resp = req
.send()
.await
.map_err(|e| format!("request failed: {e}"))?;
check_status(resp).await
}
async fn post_json(
client: &Client,
url: &str,
body: &Value,
headers: &[(String, String)],
) -> Result<(), String> {
let mut req = client.post(url).json(body);
for (k, v) in headers {
req = req.header(k, v);
}
let resp = req
.send()
.await
.map_err(|e| format!("request failed: {e}"))?;
check_status(resp).await
}
async fn check_status(resp: reqwest::Response) -> Result<(), String> {
let status = resp.status();
if status.is_success() {
return Ok(());
}
let body = resp.text().await.unwrap_or_default();
let excerpt: String = body.chars().take(200).collect();
Err(format!("HTTP {status}: {excerpt}"))
}
fn hmac_sha256_hex(key: &[u8], data: &[u8]) -> String {
let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(data);
let bytes = mac.finalize().into_bytes();
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
use std::fmt::Write;
let _ = write!(out, "{b:02x}");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hmac_matches_known_vector() {
let key = [0x0bu8; 20];
let got = hmac_sha256_hex(&key, b"Hi There");
assert_eq!(
got,
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
);
}
#[test]
fn hmac_is_deterministic() {
let a = hmac_sha256_hex(b"secret", b"payload");
let b = hmac_sha256_hex(b"secret", b"payload");
assert_eq!(a, b);
assert_ne!(a, hmac_sha256_hex(b"secret", b"other"));
assert_eq!(a.len(), 64); }
}