use base64::{Engine, prelude::BASE64_STANDARD};
use rocket::{data::Outcome, http::Status, outcome::try_outcome, tokio::io::AsyncRead};
use tokio_util::bytes::{Bytes, BytesMut};
use crate::{
WebhookError,
webhooks::{
Webhook,
interface::public_key::{WebhookPublicKey, algorithms::p256::EcdsaP256Asn1},
},
};
pub struct SendGridWebhook {
public_key: Bytes,
}
impl SendGridWebhook {
pub fn with_public_key(public_key: impl AsRef<str>) -> Result<Self, base64::DecodeError> {
let public_key = Bytes::from(BASE64_STANDARD.decode(public_key.as_ref())?);
Ok(Self { public_key })
}
}
impl Webhook for SendGridWebhook {
async fn validate_body(
&self,
req: &rocket::Request<'_>,
body: impl AsyncRead + Unpin + Send + Sync,
time_bounds: (u32, u32),
) -> Outcome<'_, Vec<u8>, WebhookError> {
self.validate_with_public_key(req, body, time_bounds).await
}
}
impl WebhookPublicKey for SendGridWebhook {
type ALG = EcdsaP256Asn1;
async fn public_key(&self, _req: &rocket::Request<'_>) -> Outcome<'_, Bytes, WebhookError> {
Outcome::Success(self.public_key.clone())
}
fn expected_signature(&self, req: &rocket::Request<'_>) -> Outcome<'_, Vec<u8>, WebhookError> {
let sig_header =
try_outcome!(self.get_header(req, "X-Twilio-Email-Event-Webhook-Signature", None));
match BASE64_STANDARD.decode(sig_header) {
Ok(bytes) => Outcome::Success(bytes),
Err(_) => Outcome::Error((
Status::BadRequest,
WebhookError::InvalidHeader(format!(
"X-Twilio-Email-Event-Webhook-Signature header was not valid base64: '{sig_header}'"
)),
)),
}
}
fn message_to_verify(
&self,
req: &rocket::Request<'_>,
body: &Bytes,
time_bounds: (u32, u32),
) -> Outcome<'_, Bytes, WebhookError> {
let timestamp =
try_outcome!(self.get_header(req, "X-Twilio-Email-Event-Webhook-Timestamp", None));
try_outcome!(self.validate_timestamp(timestamp, time_bounds));
let mut timestamp_and_body = BytesMut::with_capacity(timestamp.len() + body.len());
timestamp_and_body.extend_from_slice(timestamp.as_bytes());
timestamp_and_body.extend_from_slice(body);
Outcome::Success(timestamp_and_body.freeze())
}
}