faucet_cli/notify/
channels.rs1use super::render::{self, PdAction};
9use super::spec::{PagerdutyConfig, SlackConfig, WebhookConfig};
10use crate::notify::event::NotifyEvent;
11use hmac::{Hmac, Mac};
12use reqwest::Client;
13use serde_json::Value;
14use sha2::Sha256;
15
16pub const PAGERDUTY_ENDPOINT: &str = "https://events.pagerduty.com/v2/enqueue";
18
19pub async fn send_slack(
21 client: &Client,
22 cfg: &SlackConfig,
23 event: &NotifyEvent,
24) -> Result<(), String> {
25 let body = render::slack(cfg, event);
26 post_json(client, &cfg.webhook_url, &body, &[]).await
27}
28
29pub async fn send_pagerduty(
31 client: &Client,
32 cfg: &PagerdutyConfig,
33 event: &NotifyEvent,
34 action: PdAction,
35 dedup_key: &str,
36) -> Result<(), String> {
37 let body = render::pagerduty(cfg, event, action, dedup_key);
38 let url = cfg.endpoint.as_deref().unwrap_or(PAGERDUTY_ENDPOINT);
39 post_json(client, url, &body, &[]).await
40}
41
42pub async fn send_webhook(
44 client: &Client,
45 cfg: &WebhookConfig,
46 event: &NotifyEvent,
47) -> Result<(), String> {
48 let body = render::webhook(cfg, event);
49 let raw = serde_json::to_vec(&body).map_err(|e| format!("serializing webhook body: {e}"))?;
50
51 let mut headers: Vec<(String, String)> = cfg
52 .headers
53 .iter()
54 .map(|(k, v)| (k.clone(), v.clone()))
55 .collect();
56 if let Some(secret) = &cfg.hmac_secret {
57 headers.push((
58 cfg.signature_header.clone(),
59 hmac_sha256_hex(secret.as_bytes(), &raw),
60 ));
61 }
62
63 let method = reqwest::Method::from_bytes(cfg.method.as_bytes())
64 .map_err(|_| format!("invalid HTTP method `{}`", cfg.method))?;
65 let mut req = client
66 .request(method, &cfg.url)
67 .header(reqwest::header::CONTENT_TYPE, "application/json")
68 .body(raw);
69 for (k, v) in &headers {
70 req = req.header(k, v);
71 }
72 let resp = req
73 .send()
74 .await
75 .map_err(|e| format!("request failed: {e}"))?;
76 check_status(resp).await
77}
78
79async fn post_json(
81 client: &Client,
82 url: &str,
83 body: &Value,
84 headers: &[(String, String)],
85) -> Result<(), String> {
86 let mut req = client.post(url).json(body);
87 for (k, v) in headers {
88 req = req.header(k, v);
89 }
90 let resp = req
91 .send()
92 .await
93 .map_err(|e| format!("request failed: {e}"))?;
94 check_status(resp).await
95}
96
97async fn check_status(resp: reqwest::Response) -> Result<(), String> {
100 let status = resp.status();
101 if status.is_success() {
102 return Ok(());
103 }
104 let body = resp.text().await.unwrap_or_default();
105 let excerpt: String = body.chars().take(200).collect();
106 Err(format!("HTTP {status}: {excerpt}"))
107}
108
109fn hmac_sha256_hex(key: &[u8], data: &[u8]) -> String {
111 let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepts any key length");
112 mac.update(data);
113 let bytes = mac.finalize().into_bytes();
114 let mut out = String::with_capacity(bytes.len() * 2);
115 for b in bytes {
116 use std::fmt::Write;
117 let _ = write!(out, "{b:02x}");
118 }
119 out
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn hmac_matches_known_vector() {
128 let key = [0x0bu8; 20];
130 let got = hmac_sha256_hex(&key, b"Hi There");
131 assert_eq!(
132 got,
133 "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
134 );
135 }
136
137 #[test]
138 fn hmac_is_deterministic() {
139 let a = hmac_sha256_hex(b"secret", b"payload");
140 let b = hmac_sha256_hex(b"secret", b"payload");
141 assert_eq!(a, b);
142 assert_ne!(a, hmac_sha256_hex(b"secret", b"other"));
143 assert_eq!(a.len(), 64); }
145}