CluPilotCloud/app/Console/Commands/VerifyCustomDomains.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;
}
}