const crypto = require('crypto'); const { readConfig } = require('./webhook-config'); function getHeader(headers, name) { const lower = name.toLowerCase(); for (const [key, value] of Object.entries(headers)) { if (key.toLowerCase() === lower) { if (Array.isArray(value)) return value[0]; return value; } } return undefined; } function extractSignature(headers) { const auth = getHeader(headers, 'authorization'); if (auth) { const bearerMatch = auth.match(/^Bearer\s+(.+)$/i); if (bearerMatch) return bearerMatch[1].trim(); return auth.trim(); } const sigHeaders = ['x-qiwe-signature', 'x-qiweapi-signature', 'x-signature']; for (const name of sigHeaders) { const val = getHeader(headers, name); if (val) return val; } return null; } function timingSafeEqualStrings(a, b) { if (a.length !== b.length) return false; return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)); } function verifyWebhookSignature(rawBody, headers) { const config = readConfig(); if (process.env.QIWEI_WEBHOOK_ALLOW_UNSECURED === 'true') { console.warn('[Webhook] QIWEI_WEBHOOK_ALLOW_UNSECURED=true,允许未签名回调(仅开发环境使用)'); return true; } if (!config.secret) { console.warn('[Webhook] Webhook 未配置 secret,回调已拒绝'); return false; } const signature = extractSignature(headers); if (!signature) { console.warn('[Webhook] 请求缺少签名信息'); return false; } if (timingSafeEqualStrings(signature, config.secret)) { return true; } try { const expected = crypto .createHmac('sha256', config.secret) .update(rawBody, 'utf8') .digest('hex'); if (timingSafeEqualStrings(expected, signature)) { return true; } } catch { // ignore } console.warn('[Webhook] 签名验证失败'); return false; } module.exports = { verifyWebhookSignature, extractSignature, getHeader, timingSafeEqualStrings };