102 lines
4.0 KiB
PHP
102 lines
4.0 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Actions\ReapplyInstanceAddress;
|
|
use App\Models\Instance;
|
|
use App\Services\Domains\DomainVerifier;
|
|
use Illuminate\Console\Command;
|
|
|
|
/**
|
|
* Re-reads every custom domain's proof, nightly.
|
|
*
|
|
* Asked for in exactly these words: the token must be checked again and again,
|
|
* so that nobody puts it in, passes the check, and takes it straight back out.
|
|
* At night, because a domain that is withdrawn is a domain that stops being
|
|
* served, and that is not a thing to do to somebody at eleven in the morning.
|
|
*
|
|
* Three consecutive misses before a verified domain is withdrawn. One failed
|
|
* lookup is a nameserver having a bad minute; taking a working Nextcloud
|
|
* offline over a hiccup would be far worse than the hole it closes.
|
|
*/
|
|
class VerifyCustomDomains extends Command
|
|
{
|
|
protected $signature = 'clupilot:verify-domains {--instance= : one instance uuid, for checking on demand}';
|
|
|
|
protected $description = 'Re-read the DNS proof for every custom domain and withdraw the ones that lost it';
|
|
|
|
public function handle(DomainVerifier $verifier, ReapplyInstanceAddress $reapply): int
|
|
{
|
|
$query = Instance::query()->whereNotNull('custom_domain')->whereNotNull('domain_token');
|
|
|
|
if ($uuid = $this->option('instance')) {
|
|
$query->where('uuid', $uuid);
|
|
}
|
|
|
|
$checked = 0;
|
|
$withdrawn = 0;
|
|
|
|
foreach ($query->cursor() as $instance) {
|
|
$checked++;
|
|
$present = $verifier->proofPresent($instance);
|
|
|
|
// What was true before this check, so the CHANGE can be acted on
|
|
// rather than the state. Re-applying an address is remote work on a
|
|
// live machine; doing it nightly for every domain that is simply
|
|
// still fine would be a hundred pointless runs a night.
|
|
$wasVerified = $instance->domainIsVerified();
|
|
|
|
if ($present) {
|
|
$instance->forceFill([
|
|
'domain_verified_at' => $instance->domain_verified_at ?? now(),
|
|
'domain_checked_at' => now(),
|
|
'domain_error' => null,
|
|
'domain_failures' => 0,
|
|
])->save();
|
|
|
|
// A domain that has just become provable is a domain nothing is
|
|
// serving yet: the router was written without it and Nextcloud
|
|
// does not trust it. This is where it becomes an address.
|
|
if (! $wasVerified) {
|
|
$reapply($instance);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
$failures = $instance->domain_failures + 1;
|
|
|
|
// Withdrawal clears verified_at, which is the single flag every
|
|
// consumer reads. Nothing else is deleted: the domain and its token
|
|
// stay on the row, so the customer sees what is wrong and can put
|
|
// the record back rather than starting over.
|
|
$withdraw = $instance->domain_verified_at !== null
|
|
&& $failures >= DomainVerifier::FAILURES_BEFORE_WITHDRAWAL;
|
|
|
|
$instance->forceFill([
|
|
'domain_checked_at' => now(),
|
|
'domain_error' => 'missing_txt',
|
|
'domain_failures' => $failures,
|
|
'domain_verified_at' => $withdraw ? null : $instance->domain_verified_at,
|
|
'domain_cert_ok' => $withdraw ? false : $instance->domain_cert_ok,
|
|
])->save();
|
|
|
|
if ($withdraw) {
|
|
$withdrawn++;
|
|
|
|
// The other half of the flip, and the one that actually takes
|
|
// the domain away: the router stops carrying it and Nextcloud
|
|
// stops trusting it. Clearing verified_at alone only stops us
|
|
// TELLING people about it.
|
|
$reapply($instance);
|
|
|
|
$this->warn("Withdrew {$instance->custom_domain}: proof missing on {$failures} consecutive checks.");
|
|
}
|
|
}
|
|
|
|
$this->info("Checked {$checked} domain(s), withdrew {$withdrawn}.");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|