Gå til hovedindhold

Webhooks

fremforge leverer webhooks, der er kompatible med både Forgejos egen signaturordning og den GitHub-kompatible ordning i den samme levering. Modtagere bygget til den ene eller den anden virker uden ændringer. Vælg den header, du stoler på, og verificér mod dit delte secret.

Denne side beskriver kontrakten: hvad fremforge sender, hvordan du verificerer det, og hvilke garantier du får ved genforsøg og TLS.

Hvor du konfigurerer hvad: webhooks på organisationsniveau (hvilke hændelser du abonnerer på, mål-URL, signerings-secret) opretter du i fremforges administration på /<org>/_admin/webhooks, hvor du også finder leveringshistorik og afsendelse igen. Forgejos side til hooks på organisationsniveau, /org/<org>/settings/hooks, viderestiller dertil. Forgejo-hooks på repository-niveau (gælder ét repository) sætter du op i Forgejos brugerflade på /<org>/<repo>/settings/hooks.

Hurtig reference

POST <your-webhook-url> HTTP/1.1
Host: receiver.example.com
User-Agent: fremforge-webhook/1
Content-Type: application/json
X-Forgejo-Event: push
X-Gitea-Delivery: 7c4e2ecd-79b5-4e1d-8eaa-0d1c2c8e5fbd
X-Forgejo-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-Hub-Signature-256: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-Hub-Signature: sha1=2fd4e1c67a2d28fced849ee1bb76e7391b93eb12

{<your event payload as JSON>}
HeaderBetydning
X-Forgejo-EventHændelsestype (push, pull_request, issues, release osv.)
X-Gitea-DeliveryUnikt UUID pr. leveringsforsøg, bruges til beskyttelse mod genafspilning
X-Forgejo-SignatureHMAC-SHA256 af den rå body, hex-kodet, Forgejos egen
X-Hub-Signature-256HMAC-SHA256 af den rå body, hex-kodet med præfikset sha256=, GitHub-kompatibel
X-Hub-SignatureHMAC-SHA1 af den rå body, ældre GitHub-kompatibilitet, stol ikke på den

Signeringsnøglen er det secret, du sætter på webhooken i organisationens administration. Tomt secret = ingen signatur; X-*-Signature-headerne mangler. fremforges standardpolitik for platformen afviser webhook-konfiguration uden secret i nye organisationer (fase 1.5+).

Hvorfor to signaturheadere

Modtagere bygget til GitHub tjekker som regel X-Hub-Signature-256. Modtagere bygget direkte til Forgejo / Gitea tjekker X-Forgejo-Signature. Når begge sendes i samme levering, kræver en migrering fra GitHub ingen ændringer i modtagerkoden, og nye integrationer kan vælge den form, der føles mest naturlig.

De to headere indeholder præcis den samme HMAC-SHA256-værdi, beregnet over den samme rå body. Verificér den ene af dem; lykkes begge, er forespørgslen ægte.

Verificering af en webhook, opskrifter

Alle opskrifterne nedenfor verificerer den GitHub-kompatible header X-Hub-Signature-256. Til X-Forgejo-Signature skal du blot springe trinnet over, der fjerner præfikset sha256=.

Node.js

import crypto from 'node:crypto';

function verifyFremforgeWebhook(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !signatureHeader.startsWith('sha256=')) return false;
  const provided = Buffer.from(signatureHeader.slice('sha256='.length), 'hex');
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return provided.length === expected.length &&
         crypto.timingSafeEqual(provided, expected);
}

// In an Express / Hono / Fastify route:
//   const isAuthentic = verifyFremforgeWebhook(
//     req.rawBody,                                // Buffer or string of the EXACT bytes received
//     req.headers['x-hub-signature-256'],
//     process.env.FREMFORGE_WEBHOOK_SECRET,
//   );

rawBody SKAL være præcis de bytes, der blev modtaget, før nogen JSON-parsing. Middleware, der serialiserer igen, giver en anden hash og en falsk afvisning.

Python

import hmac
import hashlib

def verify_fremforge_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header or not signature_header.startswith("sha256="):
        return False
    provided = signature_header[len("sha256="):]
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(provided, expected)

# In a Flask / FastAPI handler:
#   raw = request.get_data()  # Flask: raw bytes; do NOT parse JSON first
#   sig = request.headers.get("X-Hub-Signature-256", "")
#   if not verify_fremforge_webhook(raw, sig, os.environ["FREMFORGE_WEBHOOK_SECRET"]):
#       abort(401)

Go

package webhook

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strings"
)

func VerifyFremforge(rawBody []byte, signatureHeader, secret string) bool {
    if !strings.HasPrefix(signatureHeader, "sha256=") {
        return false
    }
    provided, err := hex.DecodeString(strings.TrimPrefix(signatureHeader, "sha256="))
    if err != nil {
        return false
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expected := mac.Sum(nil)
    return hmac.Equal(provided, expected)
}

Ruby

require 'openssl'

def verify_fremforge_webhook(raw_body, signature_header, secret)
  return false unless signature_header&.start_with?('sha256=')
  expected = OpenSSL::HMAC.hexdigest('sha256', secret, raw_body)
  Rack::Utils.secure_compare(signature_header.sub('sha256=', ''), expected)
end

Rust

use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;

type HmacSha256 = Hmac<Sha256>;

pub fn verify_fremforge_webhook(
    raw_body: &[u8],
    signature_header: &str,
    secret: &[u8],
) -> bool {
    let Some(hex_sig) = signature_header.strip_prefix("sha256=") else {
        return false;
    };
    let Ok(provided) = hex::decode(hex_sig) else {
        return false;
    };
    let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC key");
    mac.update(raw_body);
    let expected = mac.finalize().into_bytes();
    expected.as_slice().ct_eq(&provided).into()
}

Beskyttelse mod genafspilning

HMAC-signaturen beviser ægthed (indholdet kom fra fremforge med kendskab til dit delte secret), men ikke friskhed. En angriber, der opsnapper en gyldig signeret webhook, kan afspille den igen og igen, indtil secret’et roteres.

Brug UUID’et i X-Gitea-Delivery som forsvar mod genafspilning:

  • Hold en lille cache (Redis, hukommelse, endda SQLite) med de seneste leverings-id’er.
  • Afvis enhver indgående webhook, hvis leverings-id allerede ligger i cachen.
  • Giv cachen en TTL på 24 timer. Alle fremforges genforsøg ligger inden for cirka 7,5 timer, så det vindue dækker dem; ældre genafspilninger er ikke realistiske i forhold til rotationstakten for secret’et.

Spring kun beskyttelsen mod genafspilning over, hvis din handler er fuldt idempotent (den samme levering anvendt to gange giver den samme sluttilstand). Idempotente handlere er den reneste løsning; en genafspilningscache er reserveløsningen, når handleren ikke er det.

Krav om TLS

fremforge leverer kun webhooks over HTTPS. Modtager-URL’er med HTTP afvises allerede, når webhooken konfigureres. Modtageren skal:

  • Levere et gyldigt TLS-certifikat for det konfigurerede værtsnavn (enhver offentlig CA virker: Let’s Encrypt, ZeroSSL, ACM, kommercielle).
  • Understøtte TLS 1.2 eller nyere.
  • Ikke være afhængig af SNI-tricks, der kun virker i browsere; fremforges udgående TLS opfører sig som en HTTP-klient, ikke som en browser.

Selvsignerede certifikater afvises. Tester du lokalt, så brug en tunnel (ngrok, Cloudflare Tunnel, Bunny Origin Shield) med et rigtigt certifikat i stedet.

Genforsøg

fremforge regner kun et 2xx-svar som leveret. Alle andre svar forsøges igen: en 3xx (omdirigeringer følges ikke), en 4xx, en 5xx, en timeout (10 sekunder) eller en forbindelses- eller TLS-fejl. Svar 200 for at stoppe genforsøg for en hændelse, du ikke er interesseret i. Brug kun 4xx til fejlformet indhold, og regn med, at de også forsøges igen.

Plan for genforsøg: seks forsøg i alt. Efter første forsøg kommer genforsøgene 2 minutter, 5 minutter, 15 minutter, 1 time og 6 timer efter det forrige forsøg, cirka 7,5 timer i alt. Efter det sjette mislykkede forsøg markeres leveringen som dlq i leveringsloggen for webhooks i organisationens administration og forsøges ikke igen. Efter fem mislykkede leveringer i træk får organisationens faktureringskontakter en mail, der nævner destinationen.

For at genafspille en mislykket levering manuelt bruger du knappen Send igen i leveringsloggen for webhooks i organisationens administration. Der findes intet REST-endpoint til det.

Konfiguration af en webhook

Konfigurationen af webhooks findes to steder, afhængigt af hvad du vil:

  • Udgående fan-out fra fremforge til dit endpoint: organisationens administration, Integration, Webhooks (https://frem.sh/<your-org>/_admin/webhooks). Tilføj destination, signerings-secret og hændelsesfilter; se leveringernes tilstand og DLQ’en her.
  • Forgejos egne hooks pr. repository: Forgejos egen brugerflade på https://frem.sh/<your-org>/<repo>/settings/hooks. Forgejos side på organisationsniveau, https://frem.sh/org/<your-org>/settings/hooks, viderestiller til fanen Webhooks ovenfor. Det er de ældre Forgejo-hooks. fremforges system-hook for hele instansen opfanger de samme hændelser centralt, så de fleste kunder behøver ikke længere sætte hooks op pr. repository.

Sådan tilføjer du en udgående destination, som fremforge administrerer:

  1. Organisationens administration, Integration, Webhooks, Tilføj en modtager.
  2. Mål-URL, skal være https://.
  3. Content type, application/json anbefales.
  4. Secret, generér en værdi med høj entropi (≥ 32 bytes), og gem den i modtagerens secret manager. Genbrug ikke et secret på tværs af webhooks. Forskellige modtagere bør bruge forskellige secrets.
  5. Hændelser, vælg hvilke hændelser der udløser webhooken.
  6. Gem.

Webhookens secret gemmes krypteret i fremforges database. fremforge viser det aldrig igen via API’et efter oprettelsen; du roterer det ved at redigere webhooken og indtaste en ny værdi (den gamle værdi udskiftes atomisk).

En destination kan også modtage sikkerhedshændelser: nye fund, malware-match, blokerede pulls, overtrædelser af politikker og udløbne risikoaccepter. Du vælger dem til ved navn, og de kan leveres som ServiceNow-incidents. Se Webhooks til sikkerhedsalarmer.

Fejlfinding

Signaturen passer ikke

De fleste signaturfejl skyldes, at body’en serialiseres igen. Typiske syndere:

  • Express’ JSON-middleware body-parser, der kører FØR din verificering: din verificering ser et genserialiseret indhold, ikke de bytes, fremforge sendte. Løsning: fang den rå body før parsing (express.raw(), og parse derefter JSON manuelt).
  • Frameworks, der “hjælpsomt” pænt formaterer eller kanoniserer JSON, før de giver dig den: samme løsning.
  • Forskelle i tegnkodning: fremforge sender UTF-8; modtagere, der fortolker det som Latin-1, fejler.

TLS-fejl i leveringsloggen

  • unable to get local issuer certificate: din TLS-kæde er ufuldstændig; lever mellemcertifikaterne sammen med leaf-certifikatet.
  • certificate has expired: forny det, og klik derefter på Send igen.
  • tls: handshake failure: uoverensstemmelse i TLS-version eller cipher; fremforges udgående trafik understøtter kun moderne cipher suites.

Genafspilninger / dobbelte sideeffekter

Hvis din modtager hverken er idempotent eller holder styr på X-Gitea-Delivery, kan der kortvarigt opstå dubletter under genforsøg (f.eks. når fremforge leverer en webhook igen, fordi dit 200-svar gik tabt undervejs). Løs det ved at tilføje den genafspilningscache, der er beskrevet ovenfor.

Kilde

Signeringen er implementeret i opstrøms Forgejo; fremforge ændrer den ikke. Se Forgejos dokumentation om webhooks for flere detaljer om hændelsernes form. fremforges rolle er at sætte secrets med høj entropi op som standard og at gøre de verificeringsopskrifter, der er beskrevet her, tilgængelige.

Om sikkerhed for indgående webhooks (Mollie-betalinger → fremforge), se sikkerhedsmodellen. Den bruger en anden form: mønsteret URL-som-secret.