Ir al contenido

Receiving webhooks

Documentación 3 min de lectura Fuente en codefloeSolo en inglés

En esta página

Catodon sends a webhook as an HTTP POST with a JSON body, for the webhooks a user sets up under Settings > Developer > Webhook and for the ones an administrator sets up under Control Panel > Webhooks. This page is for whoever writes the receiving end: how to check that a delivery really comes from your server.

Headers

Header Value
X-Catodon-Signature t=<unix seconds>,v1=<hex>, see below. Absent when the webhook has no secret.
X-Catodon-Host The host of the sending server, for example example.social.
X-Catodon-Hook-Id The id of the webhook.
X-Misskey-Host, X-Misskey-Hook-Id The same values, kept for receivers written for Misskey.
X-Misskey-Hook-Secret The secret itself. Deprecated: it lets anyone who sees one delivery forge the next ones. Check the signature instead; this header will be removed in 27.1.0.
User-Agent Misskey-Hooks

Checking the signature

v1 is the HMAC-SHA256, in hexadecimal, of the timestamp t, a dot, and the request body, keyed with the webhook's secret:

v1 = hex(HMAC-SHA256(secret, t + "." + body))

To accept a delivery:

  1. Hash the raw bytes of the body as received, before any JSON parsing. A body parsed and serialised again will not give the same bytes, and the signature will not match.
  2. Compare your result with v1 in constant time (crypto.timingSafeEqual in Node, hmac.compare_digest in Python), never with ==.
  3. Reject a timestamp too far from your own clock, five minutes for instance, so that a delivery someone captured cannot be replayed later. The timestamp is set when the request is sent, so a retried delivery carries a fresh one.

In Node, with no dependency:

import { createHmac, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';

const secret = process.env.WEBHOOK_SECRET;
if (!secret) throw new Error('WEBHOOK_SECRET is not set');

createServer((req, res) => {
    const chunks = [];
    req.on('data', chunk => chunks.push(chunk));
    req.on('end', () => {
        const raw = Buffer.concat(chunks);
        const header = req.headers['x-catodon-signature'];
        const { t, v1 = '' } = Object.fromEntries((typeof header === 'string' ? header : '').split(',').map(part => part.split('=')));
        const expected = createHmac('sha256', secret).update(`${t}.`).update(raw).digest();
        const given = Buffer.from(v1, 'hex');
        const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
        if (!fresh || given.length !== expected.length || !timingSafeEqual(given, expected)) {
            res.writeHead(401).end();
            return;
        }
        const delivery = JSON.parse(raw);
        // ... handle delivery.type and delivery.body
        res.writeHead(204).end();
    });
}).listen(8080);

The secret

The secret is shown in the webhook's form, where its owner can copy it. When a user creates a webhook without one, or clears the field and saves, the server generates a random one. A webhook created before signatures existed with an empty secret is sent unsigned until its owner saves it once. System webhooks always have a secret, since the form requires one.

Answering

Answer with a 2xx status. A 4xx answer other than 429 is taken as final and the delivery is not retried; a 429 or 5xx answer, a timeout or a connection error is retried, up to four attempts in all.

Every attempt is logged on the webhook's edit page, with the status you answered or why no answer came. The last 30 are kept, and the daily clean deletes those older than 30 days. From there its owner can send a delivery again: the body is the same, eventId included, so a receiver that keeps the ids it has handled can skip one it already processed; only the timestamp and the signature are new.

An event that fails for good, after its retries or at once on a 4xx, counts against the webhook. After 10 such events in a row, the first of them at least an hour earlier, the webhook is disabled and its owner is notified: the account for an account webhook, every moderator and admin for a system webhook. A single success starts the count again, and turning the webhook back on clears it. Test deliveries and deliveries sent again by hand do not count.