feat: log every incoming webhook request before validation

Saves a raw entry to webhook_logs immediately on any POST to the webhook,
before HMAC check or JSON parsing. Shows HMAC status (ok/invalida/sin-firma).
Live feed now shows all traffic including rejected and malformed payloads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-22 11:39:23 -05:00
co-authored by Claude Sonnet 4.6
parent 483cc4797e
commit 454ee297e2
2 changed files with 41 additions and 11 deletions
+2 -1
View File
@@ -83,6 +83,7 @@ class DashboardController
.b-interactive{background:#ede7f6;color:#4527a0}
.b-button{background:#e3f2fd;color:#0d47a1}
.b-status{background:#f3f4f6;color:#6b7280}
.b-raw{background:#fef9c3;color:#854d0e}
.b-other{background:#f5f5f5;color:#555}
</style>
<div class="toolbar">
@@ -104,7 +105,7 @@ class DashboardController
</div>
<script>
const ICONS = {text:'&#128172;',image:'&#128247;',audio:'&#127925;',video:'&#127916;',document:'&#128196;',sticker:'&#127774;',reaction:'&#128077;',location:'&#128205;',interactive:'&#128280;',button:'&#128306;',status:'&#128202;'};
const ICONS = {text:'&#128172;',image:'&#128247;',audio:'&#127925;',video:'&#127916;',document:'&#128196;',sticker:'&#127774;',reaction:'&#128077;',location:'&#128205;',interactive:'&#128280;',button:'&#128306;',status:'&#128202;',raw:'&#128268;'};
const BADGE = t => 'b-'+(ICONS[t]?t:'other');
let paused = false;
+39 -10
View File
@@ -54,35 +54,64 @@ class WpWebhook
public static function receive(): void
{
// 0. Log raw request first (before any validation)
$rawBody = file_get_contents('php://input');
$headers = getallheaders();
self::debugLog('POST', json_encode(['headers' => $headers, 'body' => $rawBody], JSON_UNESCAPED_UNICODE));
// 1. Validar body
// Guardar raw body para los handlers
self::$currentRaw = $rawBody ?: '';
// Guardar SIEMPRE una entrada raw en webhook_logs antes de cualquier validación
self::saveRawIncoming($rawBody ?: '', $headers);
if ($rawBody === false || $rawBody === '') {
self::respond(400, ['error' => 'Body vacío']);
}
// 2. Guardar raw body para los handlers
self::$currentRaw = $rawBody;
// 3. Verificar firma HMAC-SHA256 de Meta
self::verifySignature($rawBody);
// 4. Decodificar JSON
$payload = json_decode($rawBody, true);
if (!is_array($payload)) {
self::respond(400, ['error' => 'JSON inválido']);
}
// 5. Procesar ANTES de responder
self::processEvent($payload, $rawBody);
// 6. Responder 200 a Meta (menos de 20 seg)
self::respond(200, ['status' => 'received']);
}
private static function saveRawIncoming(string $rawBody, array $headers): void
{
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$preview = mb_substr($rawBody, 0, 300);
$sigOk = 'pendiente';
$appSecret = env('WHATSAPP_APP_SECRET', '');
$hmacEnabled = env('verify_hmac_signature', '1');
if ($hmacEnabled !== '0' && $appSecret !== '') {
$sigHeader = $headers['X-Hub-Signature-256'] ?? $headers['x-hub-signature-256'] ?? '';
if (strpos($sigHeader, 'sha256=') === 0) {
$received = substr($sigHeader, 7);
$expected = hash_hmac('sha256', $rawBody, $appSecret);
$sigOk = hash_equals($expected, $received) ? 'ok' : 'invalida';
} else {
$sigOk = 'sin-firma';
}
} else {
$sigOk = 'desactivada';
}
try {
$stmt = db()->prepare("
INSERT INTO webhook_logs
(company_id, event_field, from_number, contact_name, message_type, message_preview, raw_payload)
VALUES (NULL, 'raw', ?, ?, 'raw', ?, ?)
");
$stmt->execute([$ip, "HMAC:{$sigOk}", mb_substr($preview, 0, 500), $rawBody]);
} catch (\PDOException $e) {
self::log('ERROR', 'saveRawIncoming DB: ' . $e->getMessage());
}
}
// ─── Procesamiento de eventos ─────────────────────────────────────────────
private static function processEvent(array $payload, string $raw): void