rocket_webhook/webhooks/built_in/
discord.rs

1use hex::FromHexError;
2use rocket::{data::Outcome, http::Status, outcome::try_outcome, tokio::io::AsyncRead};
3use tokio_util::bytes::{Bytes, BytesMut};
4
5use crate::{
6    WebhookError,
7    webhooks::{
8        Webhook,
9        interface::public_key::{WebhookPublicKey, algorithms::ed25519::Ed25519},
10    },
11};
12
13/// # Discord Interactions webhook
14///
15/// Looks for `X-Signature-Ed25519` and `X-Signature-Timestamp` headers.
16/// Signature should be hex Ed25519 signature of `{timestamp}{body}`
17///
18/// [Discord docs](https://discord.com/developers/docs/interactions/overview#setting-up-an-endpoint-validating-security-request-headers)
19pub struct DiscordWebhook {
20    public_key: Bytes,
21}
22
23impl DiscordWebhook {
24    /// Instantiate using the hex public key from Discord
25    pub fn with_public_key(public_key: impl AsRef<str>) -> Result<Self, FromHexError> {
26        let public_key = Bytes::from(hex::decode(public_key.as_ref())?);
27        Ok(Self { public_key })
28    }
29}
30
31impl Webhook for DiscordWebhook {
32    async fn validate_body(
33        &self,
34        req: &rocket::Request<'_>,
35        body: impl AsyncRead + Unpin + Send + Sync,
36        time_bounds: (u32, u32),
37    ) -> Outcome<'_, Vec<u8>, WebhookError> {
38        self.validate_with_public_key(req, body, time_bounds).await
39    }
40}
41
42impl WebhookPublicKey for DiscordWebhook {
43    type ALG = Ed25519;
44
45    async fn public_key(&self, _req: &rocket::Request<'_>) -> Outcome<'_, Bytes, WebhookError> {
46        Outcome::Success(self.public_key.clone())
47    }
48
49    fn expected_signature(&self, req: &rocket::Request<'_>) -> Outcome<'_, Vec<u8>, WebhookError> {
50        let sig_header = try_outcome!(self.get_header(req, "X-Signature-Ed25519", None));
51        match hex::decode(sig_header) {
52            Ok(bytes) => Outcome::Success(bytes),
53            Err(_) => Outcome::Error((
54                Status::BadRequest,
55                WebhookError::InvalidHeader(format!(
56                    "X-Signature-Ed25519 header was not valid hex: '{sig_header}'"
57                )),
58            )),
59        }
60    }
61
62    fn message_to_verify(
63        &self,
64        req: &rocket::Request<'_>,
65        body: &Bytes,
66        time_bounds: (u32, u32),
67    ) -> Outcome<'_, Bytes, WebhookError> {
68        let timestamp = try_outcome!(self.get_header(req, "X-Signature-Timestamp", None));
69        try_outcome!(self.validate_timestamp(timestamp, time_bounds));
70
71        let mut timestamp_and_body = BytesMut::with_capacity(timestamp.len() + body.len());
72        timestamp_and_body.extend_from_slice(timestamp.as_bytes());
73        timestamp_and_body.extend_from_slice(body);
74
75        Outcome::Success(timestamp_and_body.freeze())
76    }
77}