Files
novaix-releases/integrations/webhook-examples/python/receiver.py
T

103 lines
3.1 KiB
Python

"""
NovaIx Webhook 接收端 - Flask 示例
部署方式:
pip install flask
export NOVAIX_CALLBACK_SECRET=<创建 API 密钥时返回的 callback_secret>
flask --app receiver run --host 0.0.0.0 --port 8000
生产部署建议用 gunicorn / uvicorn + nginx 反向代理。
关键点:
- 必须用原始请求体(request.get_data())计算 HMAC,不能用 request.get_json() 后的结果
- 必须用 hmac.compare_digest 做恒定时间比较
- 必须用 HTTPS(生产环境)
"""
import hashlib
import hmac
import logging
import os
from flask import Flask, abort, request
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 # 64 KB,挡掉大 payload 攻击
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("novaix-callback")
SECRET = os.environ.get("NOVAIX_CALLBACK_SECRET", "")
def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
if not signature or not secret:
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/callback")
def callback():
if not SECRET:
log.error("服务端未配置 NOVAIX_CALLBACK_SECRET")
abort(500)
# 1. 读取原始请求体
raw_body = request.get_data()
# 2. 验证签名
signature = request.headers.get("X-Novaix-Signature", "")
if not verify_signature(raw_body, signature, SECRET):
log.warning("签名验证失败")
abort(401)
# 3. 解析 payload(验签后才解析)
payload = request.get_json(silent=True) or {}
event = payload.get("event", "")
task_id = payload.get("task_id", 0)
task_type = payload.get("task_type", "")
external_id = payload.get("external_id", "")
status = payload.get("status", "")
# 4. 幂等处理(伪代码)
# if already_processed(task_id):
# return "ok", 200
if task_type == "create_instance":
if status == "completed":
data = payload.get("data") or {}
ip = data.get("ip_address", "")
hostname = data.get("hostname", "")
log.info("实例开通成功 external_id=%s ip=%s", external_id, ip)
# mark_service_active(external_id, ip, hostname)
else:
err = payload.get("result", "未知错误")
log.warning("实例开通失败 external_id=%s reason=%s", external_id, err)
# mark_service_failed(external_id, err)
elif task_type == "delete_instance":
log.info("实例删除完成 external_id=%s", external_id)
# mark_service_terminated(external_id)
elif task_type in {
"start_instance",
"stop_instance",
"restart_instance",
"freeze_instance",
"unfreeze_instance",
}:
log.info("操作完成 task_type=%s status=%s external_id=%s", task_type, status, external_id)
else:
log.info("未处理的任务类型 task_type=%s", task_type)
# 5. mark_processed(task_id)
# 6. 返回 2xx 表示成功接收(NovaIx 不会重试)
return "ok", 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)