diff --git a/updater/src/index.js b/updater/src/index.js index cbe3d8c..139ad37 100644 --- a/updater/src/index.js +++ b/updater/src/index.js @@ -6,6 +6,13 @@ import { promisify } from 'util' const execAsync = promisify(exec) const app = Fastify({ logger: true }) + +// Capture the exact request bytes for webhook HMAC verification — +// re-serialising the parsed body does not round-trip Forgejo's payload. +app.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => { + req.rawBody = body + try { done(null, JSON.parse(body)) } catch (err) { done(err) } +}) const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || '' const SSH_KEY = process.env.SSH_KEY_PATH || '/root/.ssh/id_ed25519' const FORGEJO_URL = process.env.FORGEJO_URL || 'https://git.pterois.co.uk' @@ -75,8 +82,11 @@ const CACHE_TTL = 5 * 60 * 1000 function verifySignature(body, signature) { if (!WEBHOOK_SECRET) return true - const expected = `sha256=${crypto.createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex')}` - return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature || '')) + // Forgejo/Gitea send a bare hex digest; GitHub prefixes it with "sha256=" + const provided = (signature || '').replace(/^sha256=/, '') + const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex') + if (provided.length !== expected.length) return false + return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided)) } async function sshGet(host, cmd) { @@ -203,6 +213,8 @@ app.post('/deploy/:repo', async (request, reply) => { app.post('/webhook', { config: { rawBody: true } }, async (request, reply) => { const sig = request.headers['x-hub-signature-256'] + || request.headers['x-forgejo-signature'] + || request.headers['x-gitea-signature'] const rawBody = request.rawBody || JSON.stringify(request.body) if (!verifySignature(rawBody, sig)) return reply.status(401).send({ error: 'Invalid signature' })