CluPilotCloud/app/Services/Proxy/CertificateSweep.php

120 lines
3.8 KiB
PHP

<?php
namespace App\Services\Proxy;
use App\Models\ProxyHost;
use App\Support\KnownHostnames;
/**
* Bringt die Liste auf den Stand der Installation und misst, was jeder Name
* gerade wirklich ausliefert.
*
* Zwei getrennte Hälften, mit Absicht: `sync()` kostet nichts und darf beim
* Öffnen der Seite laufen, `check()` geht ins Netz und läuft auf Knopfdruck
* oder nach Zeitplan. Beim Seitenaufruf zu messen hieße, den Betreiber auf ein
* halbes Dutzend TLS-Handschläge warten zu lassen — und einer davon ist immer
* der Name, der gerade nicht auflöst.
*/
class CertificateSweep
{
public function __construct(private CertificateInspector $inspector) {}
/**
* Die Namen aus der Umgebung in die Liste übernehmen.
*
* @return int wie viele Zeilen neu hinzugekommen sind
*/
public function sync(): int
{
$bekannt = KnownHostnames::all();
$this->pruneVanished($bekannt);
$added = 0;
foreach ($bekannt as $hostname => $purpose) {
$existing = ProxyHost::query()->where('hostname', $hostname)->first();
if ($existing === null) {
ProxyHost::create([
'hostname' => $hostname,
'purpose' => $purpose,
'source' => 'config',
]);
$added++;
continue;
}
// Ein von Hand angelegter Name, der inzwischen auch in der Umgebung
// steht, bleibt „manual" — sonst verlöre der Betreiber die
// Möglichkeit, ihn wieder zu entfernen, ohne je etwas dafür getan
// zu haben.
if ($existing->isFromConfig() && $existing->purpose !== $purpose) {
$existing->forceFill(['purpose' => $purpose])->save();
}
}
return $added;
}
/**
* Zeilen, die aus der Umgebung kamen und dort nicht mehr stehen.
*
* Zwei Fälle, und sie brauchen zwei Antworten:
*
* - Die Zeile hatte NIE ein Zertifikat. Dann war sie ein Fehlgriff — so
* kam `127.0.0.1` aus `ADMIN_HOSTS` in die Liste, wo eine nackte IP als
* Wiederzugang stehen darf, für die es aber nie ein Zertifikat geben
* wird. Sie geht weg; ein dauerhaft roter Eintrag, den niemand beheben
* kann, macht die Übersicht unlesbar.
* - Die Zeile HAT ein Zertifikat. Dann wird sie zu einem Eintrag von Hand,
* statt zu verschwinden. Das Zertifikat läuft ja weiter, und etwas mit
* einer laufenden Frist still aus einer Übersicht zu nehmen ist das
* Gegenteil einer Übersicht — der Betreiber entfernt sie selbst.
*
* @param array<string, string> $bekannt
*/
private function pruneVanished(array $bekannt): void
{
$verschwunden = ProxyHost::query()
->where('source', 'config')
->whereNotIn('hostname', array_keys($bekannt))
->get();
foreach ($verschwunden as $host) {
if ($host->hasCertificate()) {
$host->forceFill(['source' => 'manual'])->save();
continue;
}
$host->delete();
}
}
/** @return array{checked: int, failing: int} */
public function check(): array
{
$checked = 0;
$failing = 0;
foreach (ProxyHost::query()->get() as $host) {
$result = $this->inspector->inspect($host->hostname);
$host->forceFill([
'certificate_expires_at' => $result['expires_at'],
'certificate_checked_at' => now(),
'certificate_error' => $result['error'],
])->save();
$checked++;
if ($result['error'] !== null) {
$failing++;
}
}
return ['checked' => $checked, 'failing' => $failing];
}
}