Files
oauth/app/Support/WebhookEndpoints.php
2026-07-27 17:20:02 +02:00

82 lines
2.4 KiB
PHP

<?php
namespace App\Support;
use App\Models\WebhookEndpoint;
/**
* Compartido por OAuth\WebhookController (sesión) y Api\WebhookController
* (token) — mismo patrón que AnalyticsSummary/AuditLog::paginatedFor/UserSessions.
*/
class WebhookEndpoints
{
public const EVENTS = ['token.revoked', 'user.deactivated'];
/**
* Protección anti-SSRF: rechaza URLs que resuelven a IPs privadas,
* loopback o reservadas (incluye 169.254.169.254, el endpoint de
* metadata de instancia en AWS/GCP/Azure). Se llama tanto al
* crear/editar un webhook como, de nuevo, justo antes de cada envío
* (SendWebhookJob) — un dominio público al registrarse puede resolver
* a una IP interna más tarde (DNS rebinding).
*/
public static function isSafeUrl(string $url): bool
{
$parts = parse_url($url);
if (! $parts || empty($parts['host']) || ! in_array($parts['scheme'] ?? '', ['http', 'https'], true)) {
return false;
}
$host = $parts['host'];
if (filter_var($host, FILTER_VALIDATE_IP)) {
return self::isPublicIp($host);
}
$records = array_merge(
@dns_get_record($host, DNS_A) ?: [],
@dns_get_record($host, DNS_AAAA) ?: [],
);
if (empty($records)) {
return false;
}
foreach ($records as $record) {
$ip = $record['ip'] ?? $record['ipv6'] ?? null;
if (! $ip || ! self::isPublicIp($ip)) {
return false;
}
}
return true;
}
private static function isPublicIp(string $ip): bool
{
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
}
public static function present(WebhookEndpoint $endpoint): array
{
$last = $endpoint->deliveries()->latest('created_at')->first();
return [
'id' => $endpoint->id,
'client_id' => $endpoint->client_id,
'url' => $endpoint->url,
'events' => $endpoint->events,
'active' => $endpoint->active,
'created_at' => $endpoint->created_at,
'last_delivery' => $last ? [
'event' => $last->event,
'response_status' => $last->response_status,
'delivered_at' => $last->delivered_at,
'created_at' => $last->created_at,
] : null,
];
}
}