use bytes::Bytes;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::{error::WiError, types::Event};
type HmacSha256 = Hmac<Sha256>;
const SIGNATURE_HEADER: &str = "x-wi-signature";
const SIGNATURE_V2_HEADER: &str = "x-wi-signature-v2";
pub const DEFAULT_TOLERANCE_SECONDS: u64 = 300;
#[derive(Debug, Clone, Copy)]
pub struct VerifyOptions {
pub tolerance_seconds: u64,
pub now: Option<i64>,
}
impl Default for VerifyOptions {
fn default() -> Self {
Self {
tolerance_seconds: DEFAULT_TOLERANCE_SECONDS,
now: None,
}
}
}
fn header_value<'a, K: AsRef<str>, V: AsRef<str>>(
headers: &'a [(K, V)],
name: &str,
) -> Option<&'a str> {
headers
.iter()
.find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_ref())
}
pub fn verify_webhook<K: AsRef<str>, V: AsRef<str>>(
body: &[u8],
headers: &[(K, V)],
secret: &[u8],
options: VerifyOptions,
) -> bool {
let Some(v2) = header_value(headers, SIGNATURE_V2_HEADER) else {
return header_value(headers, SIGNATURE_HEADER)
.is_some_and(|legacy| verify_signature(body, legacy, secret));
};
let (mut t, mut v1) = (None, None);
for part in v2.split(',') {
match part.trim().split_once('=') {
Some(("t", v)) => t = Some(v),
Some(("v1", v)) => v1 = Some(v),
_ => {}
}
}
let (Some(t_str), Some(v1)) = (t, v1) else {
return false;
};
let Ok(t_val) = t_str.parse::<i64>() else {
return false;
};
let Ok(expected) = hex::decode(v1) else {
return false;
};
let now = options.now.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
});
if now.abs_diff(t_val) > options.tolerance_seconds {
return false;
}
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC accepts any key length");
mac.update(t_str.as_bytes());
mac.update(b".");
mac.update(body);
mac.verify_slice(&expected).is_ok()
}
pub fn verify_signature(body: &[u8], signature: &str, secret: &[u8]) -> bool {
let hex_part = match signature.strip_prefix("sha256=") {
Some(h) => h,
None => return false,
};
let expected = match hex::decode(hex_part) {
Ok(b) => b,
Err(_) => return false,
};
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC accepts any key length");
mac.update(body);
mac.verify_slice(&expected).is_ok()
}
pub fn parse_event(body: &[u8]) -> Result<Event, WiError> {
serde_json::from_slice(body).map_err(WiError::Serialize)
}
pub struct WebhookPayload {
pub event: Event,
pub raw: Bytes,
}
impl WebhookPayload {
pub fn from_parts(
body: Bytes,
signature: &str,
secret: &[u8],
) -> Option<Result<Self, WiError>> {
if !verify_signature(&body, signature, secret) {
return None;
}
Some(parse_event(&body).map(|event| WebhookPayload { event, raw: body }))
}
pub fn from_headers<K: AsRef<str>, V: AsRef<str>>(
body: Bytes,
headers: &[(K, V)],
secret: &[u8],
options: VerifyOptions,
) -> Option<Result<Self, WiError>> {
if !verify_webhook(&body, headers, secret, options) {
return None;
}
Some(parse_event(&body).map(|event| WebhookPayload { event, raw: body }))
}
pub const HEADER: &'static str = SIGNATURE_HEADER;
}