wowlab_sentinel/http/routes/webhooks/
mod.rs1mod deploy;
2
3use std::sync::Arc;
4
5use axum::{Router, routing::post};
6use hmac::{Hmac, Mac};
7use sha2::Sha256;
8
9use crate::state::ServerState;
10
11pub(super) fn router() -> Router<Arc<ServerState>> {
12 Router::new().route("/webhooks/deploy", post(deploy::handle))
13}
14
15fn is_branch_allowed(filter: &[String], branch: &str) -> bool {
16 filter.is_empty() || filter.iter().any(|b| b == branch)
17}
18
19fn constant_time_eq(a: &str, b: &str) -> bool {
20 a.len() == b.len()
21 && a.bytes()
22 .zip(b.bytes())
23 .fold(0u8, |acc, (x, y)| acc | (x ^ y))
24 == 0
25}
26
27fn verify_signature(secret: &[u8], signature: &str, body: &[u8]) -> bool {
28 let Some(expected) = signature.strip_prefix("sha256=") else {
29 return false;
30 };
31
32 let Ok(mut mac) = Hmac::<Sha256>::new_from_slice(secret) else {
33 return false;
34 };
35
36 mac.update(body);
37 let computed = hex::encode(mac.finalize().into_bytes());
38
39 constant_time_eq(&computed, expected)
40}
41
42#[cfg(test)]
43mod tests {
44 use googletest::prelude::*;
45
46 use super::*;
47
48 #[gtest]
49 fn branch_filter_allows_every_branch_when_empty() -> Result<()> {
50 verify_true!(is_branch_allowed(&[], "feature/cleanup"))?;
51
52 Ok(())
53 }
54
55 #[gtest]
56 fn branch_filter_requires_an_exact_match() -> Result<()> {
57 let filter = vec!["main".to_string(), "release".to_string()];
58
59 verify_true!(is_branch_allowed(&filter, "main"))?;
60 verify_false!(is_branch_allowed(&filter, "main-old"))?;
61
62 Ok(())
63 }
64
65 #[gtest]
66 fn signature_verification_accepts_valid_hmac_and_rejects_tampering() -> Result<()> {
67 let secret = b"secret";
68 let body = b"payload";
69 let mut mac = Hmac::<Sha256>::new_from_slice(secret).or_fail()?;
70
71 mac.update(body);
72 let signature = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
73
74 verify_true!(verify_signature(secret, &signature, body))?;
75 verify_false!(verify_signature(secret, &signature, b"tampered"))?;
76 verify_false!(verify_signature(secret, "missing-prefix", body))?;
77
78 Ok(())
79 }
80}