Crate rocket_webhook

Crate rocket_webhook 

Source
Expand description

⚠️ This crate is in development and may not work as expected yet.

§Overview

Streamlined webhook validation in Rocket applications.

  • Automatically validate and deserialize webhook JSON payloads using the WebhookPayload data guard. You can also get the raw body using WebhookPayloadRaw.
  • Common webhooks included (GitHub, Slack, Stripe, Standard)
  • Easily validate custom webhooks with one of the generic builders

§Usage

use rocket::{routes, post, serde::{Serialize, Deserialize}};
use rocket_webhook::{
    RocketWebhook, WebhookPayload,
    webhooks::built_in::{GitHubWebhook, SlackWebhook},
};

#[rocket::launch]
fn rocket() -> _ {
    // Build the webhook(s)
    let github_webhook = RocketWebhook::builder()
        .webhook(GitHubWebhook::with_secret(b"my-github-secret"))
        .build();
    let slack_webhook = RocketWebhook::builder()
        .webhook(SlackWebhook::with_secret(b"my-slack-secret"))
        .build();

    // Store the webhook(s) in Rocket state
    let rocket = rocket::build()
        .manage(github_webhook)
        .manage(slack_webhook)
        .mount("/", routes![github_route]);

    rocket
}

/// JSON payload to deserialize
#[derive(Debug, Serialize, Deserialize)]
struct GithubPayload {
    action: String,
}

// use the `WebhookPayload` data guard in a route handler
#[post("/api/webhooks/github", data = "<payload>")]
async fn github_route(
    payload: WebhookPayload<'_, GithubPayload, GitHubWebhook>,
) -> &'static str {
    payload.data; // access the validated webhook payload
    payload.headers; // access the webhook headers

    "OK"
}

§Custom webhooks

If you’re using a webhook provider that is not built-in, there are two ways to integrate them:

§Use generic builder

This is the preferred (and simpler) approach - use one of the generic webhook builders to build a webhook for your provider/service. For example, here is a custom webhook that expects a hex-encoded HMAC SHA256 signature in the Foo-Signature-256 header.

use rocket_webhook::{WebhookError, webhooks::generic::Hmac256Webhook};

let my_webhook = Hmac256Webhook::builder()
    .secret("my-secret")
    .expected_signatures(|req| {
        req.headers()
            .get_one("Foo-Signature-256")
            .and_then(|header| hex::decode(header).ok())
            .map(|header| vec![header])
    })
    .build();

§Implement webhook traits

If a generic builder is not available, you can directly implement one of the signature traits along with the Webhook trait. See the implementations in webhooks::built_in for examples.

§Handling errors

By default, the webhook data guards will return Bad Request (400) for invalid requests (e.g. missing headers) and Unauthorized (401) for signature validation failures. Rocket’s error responses can be overridden using catchers scoped to a specific path.

If you need more control over how to process and respond to webhook errors, you can wrap the data guards with a Result, using the WebhookError as the Error type. You can then match on the result and handle the response as desired.

use rocket::{routes, post, serde::{Serialize, Deserialize}};
use rocket::http::Status;
use rocket_webhook::{
    WebhookError, WebhookPayload,
    webhooks::built_in::{GitHubWebhook},
};

#[post("/api/webhooks/github", data = "<payload_result>")]
async fn github_route(
    payload_result: Result<WebhookPayload<'_, GithubPayload, GitHubWebhook>, WebhookError>,
) -> (Status, &'static str) {
    match payload_result {
        Ok(payload) => (Status::Ok, "Yay!"),
        Err(err) => match err {
            WebhookError::Signature(_) => (Status::Unauthorized, "Yikes!"),
            _ => (Status::UnprocessableEntity, "Oof!")
        }
    }
}

/// Payload to deserialize
#[derive(Debug, Serialize, Deserialize)]
struct GithubPayload {
    action: String,
}

§Multiple with same type

If you want to receive webhooks using multiple accounts/keys from the same built-in or generic webhook, you’ll need to pass in a marker struct when building the webhooks and using the data guards. This is needed to distinguish between the two webhooks in Rocket’s internal state.

use rocket::{get, routes};
use rocket_webhook::{
    RocketWebhook, WebhookPayloadRaw, webhooks::built_in::SlackWebhook,
};

// Create a marker struct for each account/key
struct SlackAccount1;
struct SlackAccount2;

fn two_slack_accounts() {
    // Use the `builder_with_marker` function
    let slack_1 = RocketWebhook::builder_with_marker()
        .webhook(SlackWebhook::with_secret("slack-1-secret"))
        .marker(SlackAccount1) // pass in the marker here
        .build();
    let slack_2 = RocketWebhook::builder_with_marker()
        .webhook(SlackWebhook::with_secret("slack-2-secret"))
        .marker(SlackAccount2) // pass in the marker here
        .build();

    let rocket = rocket::build()
        .manage(slack_1)
        .manage(slack_2)
        .mount("/", routes![slack1_route, slack2_route]);
}

// Use the marker struct as the last type parameter in the data guard:

#[get("/slack-1", data = "<payload>")]
async fn slack1_route(payload: WebhookPayloadRaw<'_, SlackWebhook, SlackAccount1>) -> Vec<u8> {
    payload.data
}

#[get("/slack-2", data = "<payload>")]
async fn slack2_route(payload: WebhookPayloadRaw<'_, SlackWebhook, SlackAccount2>) -> Vec<u8> {
    payload.data
}

Modules§

webhooks
Webhook traits and implementations

Structs§

RocketWebhook
Webhook configuration stored in Rocket state.
WebhookPayload
Data guard to validate and deserialize the JSON body of webhook type W into the T type. The W webhook configuration must be in Rocket state using RocketWebhook.
WebhookPayloadRaw
Data guard to validate a webhook and get the raw body. The W webhook configuration must be in Rocket state using RocketWebhook.

Enums§

WebhookError
Possible errors when receiving a webhook