> */ private $resolve; /** * The resolver is injectable so the tests can answer without touching the * network — a suite that depends on real DNS fails on a train. * * @param null|callable(string): array> $resolve */ public function __construct(?callable $resolve = null) { $this->resolve = $resolve ?? static fn (string $name) => dns_get_record($name, DNS_TXT) ?: []; } /** The record name a customer has to create. */ public function recordName(string $domain): string { return self::RECORD_PREFIX.'.'.ltrim($domain, '.'); } /** The value it has to hold. */ public function recordValue(Instance $instance): string { return self::VALUE_PREFIX.$instance->domain_token; } /** * A fresh token. Called when a domain is set, and again whenever it * changes — a new domain must not be provable with the previous one's * token, or a customer who once verified example.com could claim any * domain afterwards without touching its DNS. */ public function newToken(): string { return Str::lower(Str::random(32)); } /** * Is the proof in place right now? * * Every TXT value at the name is considered, not just the first: a domain * may carry several TXT records at one name (SPF, other verifications), and * DNS returns them in no particular order. */ public function proofPresent(Instance $instance): bool { if (blank($instance->custom_domain) || blank($instance->domain_token)) { return false; } $expected = $this->recordValue($instance); foreach (($this->resolve)($this->recordName($instance->custom_domain)) as $record) { // dns_get_record splits long values into `entries`; `txt` is the // joined form. Both are checked because which one is populated // depends on the value's length, and a token is short enough to // land in either. $values = array_merge([(string) ($record['txt'] ?? '')], (array) ($record['entries'] ?? [])); foreach ($values as $value) { if (trim($value) === $expected) { return true; } } } return false; } }