codoseo_notify/
webhook.rs1use hmac::{Hmac, KeyInit, Mac};
8use serde_json::{Value, json};
9use sha2::Sha256;
10
11use crate::message::AlertMessage;
12
13pub const SIGNATURE_HEADER: &str = "X-CodoSEO-Signature";
14pub const TIMESTAMP_HEADER: &str = "X-CodoSEO-Timestamp";
15
16pub fn payload(msg: &AlertMessage) -> Value {
19 let changes: Vec<Value> = msg
20 .items
21 .iter()
22 .map(|i| {
23 json!({
24 "severity": i.severity,
25 "kind": i.kind,
26 "label": i.kind_label,
27 "url": i.url,
28 "before": i.before,
29 "after": i.after,
30 })
31 })
32 .collect();
33 json!({
34 "event": msg.kind.as_str(),
35 "site": msg.site_domain,
36 "dashboard_url": msg.site_url,
37 "crawl_id": msg.crawl_id,
38 "changes": changes,
39 "more": msg.more,
40 })
41}
42
43fn mac(secret: &str, timestamp: u64, body: &[u8]) -> Hmac<Sha256> {
44 let mut mac =
45 Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("HMAC accepts keys of any length");
46 mac.update(timestamp.to_string().as_bytes());
47 mac.update(b".");
48 mac.update(body);
49 mac
50}
51
52pub fn sign(secret: &str, timestamp: u64, body: &[u8]) -> String {
54 format!(
55 "sha256={}",
56 hex::encode(mac(secret, timestamp, body).finalize().into_bytes())
57 )
58}
59
60pub fn verify(secret: &str, timestamp: u64, body: &[u8], signature: &str) -> bool {
62 let Some(hex_sig) = signature.strip_prefix("sha256=") else {
63 return false;
64 };
65 let Ok(bytes) = hex::decode(hex_sig) else {
66 return false;
67 };
68 mac(secret, timestamp, body).verify_slice(&bytes).is_ok()
69}