Skip to content

Commit 71005cb

Browse files
committed
feat: add custom webhooks
1 parent 6c9fa0c commit 71005cb

File tree

3 files changed

+76
-1
lines changed

3 files changed

+76
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,9 @@ When a `secret` is configured in the webhook:
131131
GRHooks currently supports webhooks from:
132132

133133
- GitHub
134+
- Gitlab
135+
- [Custom](./crates/origin/README.md)
134136

135137
Plans to support:
136138

137-
- GitLab
138139
- Bitbucket

crates/origin/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ pub use crate::errors::Error;
88
mod errors;
99
mod github;
1010
mod gitlab;
11+
mod webhook;
1112

1213
#[derive(Clone, Copy, Debug, Default, Deserialize)]
1314
#[serde(rename_all = "lowercase")]
1415
pub enum Origin {
1516
#[default]
1617
GitHub,
1718
GitLab,
19+
Webhook,
1820
}
1921

2022
impl<'a> TryFrom<&'a HeaderMap> for Origin {
@@ -25,6 +27,8 @@ impl<'a> TryFrom<&'a HeaderMap> for Origin {
2527
Ok(Origin::GitHub)
2628
} else if headers.contains_key("X-Gitlab-Event") {
2729
Ok(Origin::GitLab)
30+
} else if headers.contains_key("X-Webhook-Event") {
31+
Ok(Origin::Webhook)
2832
} else {
2933
Err(Self::Error::MissingHeader("X-*-Event"))
3034
}
@@ -47,13 +51,15 @@ impl WebhookOrigin for Origin {
4751
match self {
4852
Origin::GitHub => github::GitHubValidator.validate_headers(headers),
4953
Origin::GitLab => gitlab::GitLabValidator.validate_headers(headers),
54+
Origin::Webhook => webhook::WebhookValidator.validate_headers(headers),
5055
}
5156
}
5257

5358
fn extract_event_type(&self, headers: &HeaderMap) -> Result<String, Error> {
5459
match self {
5560
Origin::GitHub => github::GitHubValidator.extract_event_type(headers),
5661
Origin::GitLab => gitlab::GitLabValidator.extract_event_type(headers),
62+
Origin::Webhook => webhook::WebhookValidator.extract_event_type(headers),
5763
}
5864
}
5965

@@ -66,6 +72,7 @@ impl WebhookOrigin for Origin {
6672
match self {
6773
Origin::GitHub => github::GitHubValidator.validate_signature(headers, secret, body),
6874
Origin::GitLab => gitlab::GitLabValidator.validate_signature(headers, secret, body),
75+
Origin::Webhook => webhook::WebhookValidator.validate_signature(headers, secret, body),
6976
}
7077
}
7178
}

crates/origin/src/webhook.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use axum::http::HeaderMap;
2+
use hmac::{Hmac, Mac};
3+
use sha1::Sha1;
4+
use sha2::Sha256;
5+
6+
use crate::{Error, WebhookOrigin};
7+
8+
pub struct WebhookValidator;
9+
10+
impl WebhookOrigin for WebhookValidator {
11+
fn validate_headers(&self, headers: &HeaderMap) -> Result<(), Error> {
12+
const REQUIRED_HEADERS: [&str; 3] = ["X-Webhook-ID", "X-Webhook-Event", "User-Agent"];
13+
14+
for header in REQUIRED_HEADERS {
15+
if !headers.contains_key(header) {
16+
return Err(Error::MissingHeader(header));
17+
}
18+
}
19+
20+
Ok(())
21+
}
22+
23+
fn extract_event_type(&self, headers: &HeaderMap) -> Result<String, Error> {
24+
headers
25+
.get("X-Webhook-Event")
26+
.and_then(|v| v.to_str().ok())
27+
.map(ToString::to_string)
28+
.ok_or(Error::MissingHeader("X-Webhook-Event"))
29+
}
30+
31+
fn validate_signature(
32+
&self,
33+
headers: &HeaderMap,
34+
secret: &str,
35+
body: &[u8],
36+
) -> Result<(), Error> {
37+
let (expected_signature, signature) = if let Some(signature) = headers
38+
.get("X-Webhook-Signature-256")
39+
.and_then(|v| v.to_str().ok())
40+
{
41+
let signature = signature.trim_start_matches("sha256=");
42+
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
43+
.map_err(|_| Error::InvalidSignature)?;
44+
mac.update(body);
45+
let expected_signature = hex::encode(mac.finalize().into_bytes());
46+
(expected_signature, signature)
47+
} else {
48+
let signature = headers
49+
.get("X-Webhook-Signature")
50+
.and_then(|v| v.to_str().ok())
51+
.ok_or(Error::MissingHeader("X-Webhook-Signature"))?;
52+
let signature = signature.trim_start_matches("sha1=");
53+
let mut mac = Hmac::<Sha1>::new_from_slice(secret.as_bytes())
54+
.map_err(|_| Error::InvalidSignature)?;
55+
mac.update(body);
56+
let expected_signature = hex::encode(mac.finalize().into_bytes());
57+
(expected_signature, signature)
58+
};
59+
60+
if !constant_time_eq::constant_time_eq(signature.as_bytes(), expected_signature.as_bytes())
61+
{
62+
return Err(Error::InvalidSignature);
63+
}
64+
65+
Ok(())
66+
}
67+
}

0 commit comments

Comments
 (0)