120 lines
3.8 KiB
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];
|
|
}
|
|
}
|