98 lines
3.3 KiB
JavaScript
98 lines
3.3 KiB
JavaScript
/**
|
||
* NovaIx Webhook 接收端 - Express 示例
|
||
*
|
||
* 部署方式:
|
||
* npm install express
|
||
* NOVAIX_CALLBACK_SECRET=<创建 API 密钥时返回的 callback_secret> node receiver.js
|
||
*
|
||
* 关键点:
|
||
* - express.raw() 中间件确保拿到原始请求体(不能用 express.json(),否则无法计算原始 HMAC)
|
||
* - crypto.timingSafeEqual 做恒定时间比较,避免时序攻击
|
||
* - 必须用 HTTPS(生产环境,建议放在 nginx/Caddy 反向代理后)
|
||
*/
|
||
|
||
const crypto = require('node:crypto');
|
||
const express = require('express');
|
||
|
||
const app = express();
|
||
const SECRET = process.env.NOVAIX_CALLBACK_SECRET || '';
|
||
const PORT = process.env.PORT || 8000;
|
||
|
||
function verifySignature(rawBody, signature, secret) {
|
||
if (!signature || !secret) return false;
|
||
// Buffer.from(s, 'hex') 对非偶数长度的字符串会静默截断,先做格式校验
|
||
if (!/^[0-9a-f]{64}$/i.test(signature)) return false;
|
||
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
|
||
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
|
||
}
|
||
|
||
// 用 raw 中间件拿到原始 body(Buffer),后续才能用它计算 HMAC
|
||
app.post('/callback', express.raw({ type: 'application/json', limit: '64kb' }), (req, res) => {
|
||
if (!SECRET) {
|
||
console.error('服务端未配置 NOVAIX_CALLBACK_SECRET');
|
||
return res.status(500).send('config missing');
|
||
}
|
||
|
||
// 1. 验证签名
|
||
const signature = req.header('X-Novaix-Signature') || '';
|
||
if (!verifySignature(req.body, signature, SECRET)) {
|
||
console.warn('签名验证失败');
|
||
return res.status(401).send('invalid signature');
|
||
}
|
||
|
||
// 2. 解析 payload(验签后才解析)
|
||
let payload;
|
||
try {
|
||
payload = JSON.parse(req.body.toString('utf8'));
|
||
} catch (e) {
|
||
return res.status(400).send('invalid json');
|
||
}
|
||
|
||
const { event, task_id, task_type, external_id, status } = payload;
|
||
|
||
// 3. 幂等处理(伪代码)
|
||
// if (alreadyProcessed(task_id)) {
|
||
// return res.status(200).send('ok');
|
||
// }
|
||
|
||
switch (task_type) {
|
||
case 'create_instance':
|
||
if (status === 'completed') {
|
||
const ip = payload.data?.ip_address || '';
|
||
const hostname = payload.data?.hostname || '';
|
||
console.log(`实例开通成功 external_id=${external_id} ip=${ip}`);
|
||
// markServiceActive(external_id, ip, hostname);
|
||
} else {
|
||
const err = payload.result || '未知错误';
|
||
console.warn(`实例开通失败 external_id=${external_id} reason=${err}`);
|
||
// markServiceFailed(external_id, err);
|
||
}
|
||
break;
|
||
|
||
case 'delete_instance':
|
||
console.log(`实例删除完成 external_id=${external_id}`);
|
||
// markServiceTerminated(external_id);
|
||
break;
|
||
|
||
case 'start_instance':
|
||
case 'stop_instance':
|
||
case 'restart_instance':
|
||
case 'freeze_instance':
|
||
case 'unfreeze_instance':
|
||
console.log(`操作完成 task_type=${task_type} status=${status} external_id=${external_id}`);
|
||
break;
|
||
|
||
default:
|
||
console.log(`未处理的任务类型 task_type=${task_type}`);
|
||
}
|
||
|
||
// 4. markProcessed(task_id);
|
||
|
||
// 5. 返回 2xx 表示成功接收(NovaIx 不会重试)
|
||
res.status(200).send('ok');
|
||
});
|
||
|
||
app.listen(PORT, () => {
|
||
console.log(`NovaIx callback receiver listening on :${PORT}`);
|
||
});
|