Quando un provider di telefonia VoIP (come Telnyx) inoltra chiamate verso la rete PSTN, invia notifiche HTTP asincrone (**Webhooks**) per ogni fase del ciclo di vita della chiamata (es. call.initiated, call.answered, call.hangup). Validare la firma crittografica di questi messaggi ed assicurare l'idempotenza previene attacchi di spoofing ed errate scalature dei crediti degli utenti.
1. Verifica della Firma Crittografica Telnyx in TypeScript
Telnyx firma il payload inviato con la chiave pubblica dell'account ed una marca temporale nell'header telnyx-signature-ed25519 per prevenire attacchi di tipo replay attack:
import { onRequest } from "firebase-functions/v2/https";
import * as admin from "firebase-admin";
import crypto from "crypto";
export const telnyxWebhook = onRequest({ cors: false }, async (req, res) => {
const signature = req.headers["telnyx-signature-ed25519"] as string;
const timestamp = req.headers["telnyx-timestamp"] as string;
const publicKey = process.env.TELNYX_WEBHOOK_SIGNING_PUBLIC_KEY || "";
if (!signature || !timestamp) {
res.status(400).send("Firma o timestamp mancanti.");
return;
}
// Costruzione della stringa di verifica
const payloadToVerify = `${timestamp}|${req.rawBody.toString("utf8")}`;
try {
const isVerified = crypto.verify(
null,
Buffer.from(payloadToVerify),
publicKey,
Buffer.from(signature, "base64")
);
if (!isVerified) {
res.status(403).send("Firma webhook non valida.");
return;
}
} catch (err) {
res.status(400).send("Errore nella verifica crittografica.");
return;
}
const event = req.body.data;
const eventType = event.event_type;
const callControlId = event.payload.call_control_id;
// Elaborazione idempotente su Firestore
await processCallEvent(eventType, callControlId, event.payload);
res.status(200).json({ received: true });
});
2. Aggiornamento Idempotente della Sessione di Chiamata
Per evitare di scalare i minuti due volte nel caso in cui un webhook venga re-inviato a causa di fluttuazioni di rete, registriamo lo stato dell'evento in Firestore utilizzando il call_control_id univoco:
async function processCallEvent(eventType: string, callControlId: string, payload: any) {
const callRef = admin.firestore().collection("callLogs").document(callControlId);
await admin.firestore().runTransaction(async (transaction) => {
const doc = await transaction.get(callRef);
if (eventType === "call.hangup") {
const callDurationSeconds = payload.call_duration || 0;
const billedMinutes = Math.ceil(callDurationSeconds / 60);
if (!doc.exists || !doc.data()?.isBilled) {
transaction.set(callRef, {
callControlId: callControlId,
durationSeconds: callDurationSeconds,
billedMinutes: billedMinutes,
status: "completed",
isBilled: true,
endedAt: admin.firestore.FieldValue.serverTimestamp()
}, { merge: true });
// Scalatura minuti dal wallet utente...
}
}
});
}
Conclusione
L'elaborazione transazionale ed la verifica crittografica dei webhook garantiscono un'infrastruttura di telefonia resiliente e sicura. Per richiedere una consulenza sull'architettura VoIP delle tue app mobile, scrivi a diallooyunus@gmail.com.