pub use standardwebhooks::WebhookError;
const SVIX_ID: &str = "svix-id";
const SVIX_SIGNATURE: &str = "svix-signature";
const SVIX_TIMESTAMP: &str = "svix-timestamp";
pub struct Webhook {
inner: standardwebhooks::Webhook,
}
impl Webhook {
pub fn new(secret: &str) -> Result<Self, WebhookError> {
Ok(Self {
inner: standardwebhooks::Webhook::new(secret)?,
})
}
pub fn from_bytes(secret: Vec<u8>) -> Result<Self, WebhookError> {
Ok(Self {
inner: standardwebhooks::Webhook::from_bytes(secret)?,
})
}
pub fn verify(&self, payload: &[u8], headers: &http1::HeaderMap) -> Result<(), WebhookError> {
let normalized = normalize_headers(headers);
self.inner.verify(payload, &normalized)
}
pub fn sign(
&self,
msg_id: &str,
timestamp: i64,
payload: &[u8],
) -> Result<String, WebhookError> {
self.inner.sign(msg_id, timestamp, payload)
}
}
fn normalize_headers(headers: &http1::HeaderMap) -> http1::HeaderMap {
let mut normalized = headers.clone();
if !normalized.contains_key(standardwebhooks::HEADER_WEBHOOK_ID) {
if let Some(value) = headers.get(SVIX_ID) {
normalized.insert(
http1::HeaderName::from_static(standardwebhooks::HEADER_WEBHOOK_ID),
value.clone(),
);
}
}
if !normalized.contains_key(standardwebhooks::HEADER_WEBHOOK_SIGNATURE) {
if let Some(value) = headers.get(SVIX_SIGNATURE) {
normalized.insert(
http1::HeaderName::from_static(standardwebhooks::HEADER_WEBHOOK_SIGNATURE),
value.clone(),
);
}
}
if !normalized.contains_key(standardwebhooks::HEADER_WEBHOOK_TIMESTAMP) {
if let Some(value) = headers.get(SVIX_TIMESTAMP) {
normalized.insert(
http1::HeaderName::from_static(standardwebhooks::HEADER_WEBHOOK_TIMESTAMP),
value.clone(),
);
}
}
normalized
}
#[cfg(test)]
mod tests {
use super::*;
fn make_headers(
id_key: &str,
sig_key: &str,
ts_key: &str,
signature: &str,
) -> http1::HeaderMap {
let mut headers = http1::HeaderMap::new();
headers.insert(
http1::HeaderName::try_from(id_key).unwrap(),
"msg_test123".parse().unwrap(),
);
headers.insert(
http1::HeaderName::try_from(sig_key).unwrap(),
signature.parse().unwrap(),
);
headers.insert(
http1::HeaderName::try_from(ts_key).unwrap(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string()
.parse()
.unwrap(),
);
headers
}
#[test]
fn test_verify_standard_headers() {
let secret = "whsec_C2FVsBQIhrscChlQIMV+b5sSYspob7oD";
let payload = br#"{"test": "data"}"#;
let wh = Webhook::new(secret).unwrap();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let signature = wh.sign("msg_test123", timestamp, payload).unwrap();
let headers = make_headers(
"webhook-id",
"webhook-signature",
"webhook-timestamp",
&signature,
);
let mut headers = headers;
headers.insert(
http1::HeaderName::from_static("webhook-timestamp"),
timestamp.to_string().parse().unwrap(),
);
wh.verify(payload, &headers).unwrap();
}
#[test]
fn test_verify_svix_headers() {
let secret = "whsec_C2FVsBQIhrscChlQIMV+b5sSYspob7oD";
let payload = br#"{"test": "data"}"#;
let wh = Webhook::new(secret).unwrap();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let signature = wh.sign("msg_test123", timestamp, payload).unwrap();
let mut headers = http1::HeaderMap::new();
headers.insert(
http1::HeaderName::from_static("svix-id"),
"msg_test123".parse().unwrap(),
);
headers.insert(
http1::HeaderName::from_static("svix-signature"),
signature.parse().unwrap(),
);
headers.insert(
http1::HeaderName::from_static("svix-timestamp"),
timestamp.to_string().parse().unwrap(),
);
wh.verify(payload, &headers).unwrap();
}
#[test]
fn test_invalid_signature() {
let secret = "whsec_C2FVsBQIhrscChlQIMV+b5sSYspob7oD";
let payload = br#"{"test": "data"}"#;
let wh = Webhook::new(secret).unwrap();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let mut headers = http1::HeaderMap::new();
headers.insert(
http1::HeaderName::from_static("webhook-id"),
"msg_test123".parse().unwrap(),
);
headers.insert(
http1::HeaderName::from_static("webhook-signature"),
"v1,invalid_signature_here".parse().unwrap(),
);
headers.insert(
http1::HeaderName::from_static("webhook-timestamp"),
timestamp.to_string().parse().unwrap(),
);
assert!(wh.verify(payload, &headers).is_err());
}
}