103 lines
3.1 KiB
Python
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)
|