Skip to main content

wowlab_sentinel/http/routes/webhooks/
mod.rs

1mod 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}