Files
landing-api/app/Support/Webhooks/PublicEndpoint.php
Josep Piguillem 9bdbf834f6 Send signed outgoing webhooks
Fed by the records themselves, delivered only to public HTTPS addresses
checked at every attempt, retried, and switched off after repeated failures.
A submitter's details travel only to webhooks that opted into them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:42:39 +02:00

53 lines
1.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Webhooks;
use App\Services\Media\HostResolver;
/**
* Whether a URL is somewhere this server may send a request to: HTTPS, and a
* host whose every address is public.
*
* The same rule the image downloader follows, for the same reason — the
* request leaves from inside the stack, and without it a webhook pointed at
* `http://postgres:5432` or the cloud's metadata service would be a way of
* reaching them.
*/
final class PublicEndpoint
{
public function __construct(protected HostResolver $resolver) {}
/**
* The address to connect to, or why not: `https_only`, `unresolvable` or
* `private`.
*
* @return array{0: ?string, 1: ?string} [address, reason]
*/
public function check(string $url): array
{
$parts = parse_url($url);
$host = $parts['host'] ?? null;
if (($parts['scheme'] ?? null) !== 'https' || blank($host)) {
return [null, 'https_only'];
}
$host = trim((string) $host, '[]');
$addresses = filter_var($host, FILTER_VALIDATE_IP) ? [$host] : $this->resolver->resolve($host);
if ($addresses === []) {
return [null, 'unresolvable'];
}
foreach ($addresses as $address) {
if (! filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE)) {
return [null, 'private'];
}
}
return [$addresses[0], null];
}
}