rocket_webhook/
webhooks.rs

1//! Webhook traits and implementations
2
3use rocket::{Request, data::Outcome, http::Status, tokio::io::AsyncRead};
4
5use crate::WebhookError;
6
7pub mod built_in;
8pub mod generic;
9pub mod interface;
10
11mod utils;
12
13/// Base interface for all webhooks
14pub trait Webhook {
15    /// Read body and validate webhook. If the webhook uses a timestamp, verify that it
16    /// is within the expected bounds (bounds are in unix epoch seconds).
17    fn validate_body(
18        &self,
19        req: &Request<'_>,
20        body_reader: impl AsyncRead + Unpin + Send + Sync,
21        time_bounds: (u32, u32),
22    ) -> impl Future<Output = Outcome<'_, Vec<u8>, WebhookError>> + Send + Sync;
23
24    /// Validate a timestamp against the given bounds. The default implementation assumes
25    /// that it is in Unix epoch seconds, and returns a Bad Request error if it is invalid.
26    fn validate_timestamp(
27        &self,
28        timestamp: &str,
29        (min, max): (u32, u32),
30    ) -> Outcome<'_, (), WebhookError> {
31        let unix_timestamp = timestamp.parse::<u32>().ok();
32        match unix_timestamp.map(|t| t >= min && t <= max) {
33            Some(true) => Outcome::Success(()),
34            Some(false) | None => Outcome::Error((
35                Status::BadRequest,
36                WebhookError::Timestamp(timestamp.into()),
37            )),
38        }
39    }
40
41    /// Retrieve a header that's expected for a webhook request. The default
42    /// implementation looks for the header and returns a Bad Request error if it was not provided.
43    /// It can also optionally strip a given prefix.
44    fn get_header<'r>(
45        &self,
46        req: &'r Request<'_>,
47        name: &str,
48        prefix: Option<&str>,
49    ) -> Outcome<'_, &'r str, WebhookError> {
50        let Some(mut header) = req.headers().get_one(name) else {
51            return Outcome::Error((Status::BadRequest, WebhookError::MissingHeader(name.into())));
52        };
53        if let Some(prefix) = prefix {
54            let Some(stripped) = header.strip_prefix(prefix) else {
55                return Outcome::Error((
56                    Status::BadRequest,
57                    WebhookError::InvalidHeader(format!(
58                        "'{name}' is missing prefix '{prefix}': {header}"
59                    )),
60                ));
61            };
62            header = stripped;
63        }
64        Outcome::Success(header)
65    }
66}