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>
53 lines
1.5 KiB
PHP
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];
|
|
}
|
|
}
|