انتقل إلى المحتوى الرئيسي

تحقق من توقيعات Webhook

أثبت أن Webhook جاء من Phynxa ولم يُعَد إرساله، مع شيفرة عاملة بلغتَي Node.js وPython.

ما الذي ستحققه

ستقبل نقطة الاتصال الخاصة بك فقط الطلبات التي أرسلتها Phynxa فعلًا، وسترفض الطلبات المعادة.

المتطلبات المسبقة

  • Signing secret مضبوط على الـ Webhook (راجع Webhooks). وبدونه لا تُوقَّع الطلبات.
  • الوصول إلى النص الخام للطلب، تمامًا كما استُلم، قبل أي تحليل لـ JSON.

كيف يعمل التوقيع

ترسل Phynxa ثلاث ترويسات حين يكون هناك سرّ مضبوط:

  • X-Voice-Agent-Timestamp: وقت Unix (بالثواني) الذي أُرسل فيه.
  • X-Voice-Agent-Signature-V2: قيمة HMAC-SHA256 بصيغة hex وبأحرف صغيرة للسلسلة <timestamp>.<raw body>، بمفتاح هو سرّك. استخدم هذه.
  • X-Voice-Agent-Signature: قيمة HMAC-SHA256 بصيغة hex للنص الخام وحده (الصيغة الأقدم، بلا حماية من إعادة الإرسال).

وللتحقق: أعد بناء التوقيع من سرّك وقارنه بالترويسة في زمن ثابت، ثم ارفض الطلب إذا كان الطابع الزمني قديمًا جدًا.

تحذير. وقّع البايتات الخام التي استلمتها. فإذا حلّلتَ JSON ثم كتبته مجددًا، فقد تتغير المسافات وترتيب المفاتيح ولن يتطابق التوقيع.

Python

python
import hashlib
import hmac
import time

SECRET = "whsec_your_secret"   # the Signing secret you set on the webhook
MAX_AGE_SECONDS = 300          # reject deliveries older than five minutes

def is_from_phynxa(raw_body: bytes, headers) -> bool:
    timestamp = headers.get("X-Voice-Agent-Timestamp", "")
    received = headers.get("X-Voice-Agent-Signature-V2", "")
    if not timestamp.isdigit() or not received:
        return False
    if abs(time.time() - int(timestamp)) > MAX_AGE_SECONDS:
        return False  # too old, or the clock is wrong: possible replay
    expected = hmac.new(
        SECRET.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(received, expected)

مع Flask، مرّر request.get_data() بوصفه raw_body؛ ومع FastAPI، await request.body().

Node.js

javascript
import crypto from "node:crypto";
import express from "express";

const SECRET = "whsec_your_secret";
const MAX_AGE_SECONDS = 300;
const app = express();

// Keep the raw bytes: the signature covers them exactly.
app.post("/hooks/calls", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Voice-Agent-Timestamp") ?? "";
  const received = req.get("X-Voice-Agent-Signature-V2") ?? "";
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${timestamp}.`)
    .update(req.body) // a Buffer, because of express.raw
    .digest("hex");

  const ok =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected)) &&
    age <= MAX_AGE_SECONDS;

  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString("utf8"));
  console.log(event.event, event.call_id);
  res.sendStatus(200);
});

app.listen(3000);

الاختبار

  1. شغّل نقطة الاتصال الخاصة بك في مكان يمكن الوصول إليه من الإنترنت.
  2. اضبط عنوانها وسرّها على الـ Webhook واحفظ.
  3. اختر Send test. توقّع Phynxa العينة تمامًا كتسليم حقيقي.
  4. غيّر حرفًا واحدًا من السرّ في شيفرتك وأرسل مجددًا: ينبغي أن يفشل التحقق الآن.

النتيجة المتوقعة

يقبل السرّ الصحيح تسليم الاختبار؛ ويُرفَض السرّ الخاطئ أو الطلب المتأخر بالرمز 401.

استكشاف الأخطاء

  • لا يتطابق التوقيع أبدًا. أنت توقّع نصًا أُعيد تسلسله، أو تستخدم الترويسة الخطأ (استخدم X-Voice-Agent-Signature-V2 مع الطابع الزمني)، أو أن السرّ مختلف. أعد كتابته على الـ Webhook؛ فيعرض الحقل Configured ولا يمكن قراءته.
  • تُرفَض طلبات صالحة بوصفها قديمة جدًا. تحقق من ساعة خادمك.
  • وسيط يغيّر النص. تأكد من أن لا شيء بين Phynxa وشيفرتك يعيد كتابته.

أفضل ممارسات الأمان

  • أبقِ السرّ خارج التحكم في الإصدارات، وبدّله بإدخال سرّ جديد على الـ Webhook.
  • قارن دائمًا في زمن ثابت.
  • ارفض الطوابع الزمنية المفقودة أو غير الرقمية.
  • ردّ بسرعة بـ 2xx وأنجز العمل الثقيل بعد ذلك، حتى لا تنتهي مهلة Phynxa.

أدلة ذات صلة

هل في هذه الصفحة ما لا يطابق ما تراه في لوحة التحكم؟ أخبرنا.