82 lines
2.4 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|