Add the central site registry and remote updates
Every installation now reports address and version to sites.kant-it.de every 15 minutes and picks up update orders there (ADR 123). Reports and answers are HMAC-signed with a per-installation key sent on first contact; an order can only install a published release with checksum. The next request after the file swap runs the migrations, so a remote update finishes without anyone in the admin. The platform itself lives in zentrale/ (plain PHP + SQLite, not part of the release archive): overview with outdated, silent and unassigned installations, single and bulk updates, per-site detail and history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
32cb77b72e
commit
723680b7de
39 files changed
+2355
-27
No files matched your search
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Prüft, ob CMS und Zentrale dieselbe Sprache sprechen (ADR 123).
|
||||
*
|
||||
* Die Meldung kommt aus App\Ops\Zentrale, die Antwort aus Zentrale\Api - beide echt, nur ohne
|
||||
* Netz dazwischen: Registrierung mit Schlüssel, unterschriebene Folgemeldung, Auftrag abholen,
|
||||
* Ergebnis zurückmelden, fremde Unterschrift abweisen, zweite Installation mit fremder Kennung.
|
||||
*
|
||||
* Aufruf: php scripts/zentrale-check.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Ops\ArchiveUpdate;
|
||||
use App\Ops\Backup;
|
||||
use App\Ops\CommandRunner;
|
||||
use App\Ops\Download;
|
||||
use App\Ops\Updater;
|
||||
use App\Ops\Zentrale as CmsZentrale;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
spl_autoload_register(static function (string $klasse): void {
|
||||
if (str_starts_with($klasse, 'Zentrale\\')) {
|
||||
require dirname(__DIR__).'/zentrale/app/'.substr($klasse, 9).'.php';
|
||||
}
|
||||
});
|
||||
|
||||
$tmp = str_replace('\\', '/', sys_get_temp_dir()).'/zentrale-check-'.bin2hex(random_bytes(4));
|
||||
mkdir($tmp.'/site/var', 0o775, true);
|
||||
file_put_contents($tmp.'/site/VERSION', "1.35.1\n");
|
||||
$pdo = Zentrale\Datenbank::oeffnen($tmp.'/zentrale.db');
|
||||
$api = new Zentrale\Api($pdo);
|
||||
|
||||
$fehler = 0;
|
||||
$pruefe = static function (bool $ok, string $was) use (&$fehler): void {
|
||||
echo ($ok ? 'ok ' : 'FEHLER ').$was."\n";
|
||||
$fehler += $ok ? 0 : 1;
|
||||
};
|
||||
|
||||
$ueberApi = static function (string $url, string $body, array $kopf) use ($api): array {
|
||||
$antwort = $api->melden($body, $kopf['X-KantSites-Signatur'] ?? '', '203.0.113.7');
|
||||
|
||||
return ['status' => $antwort['status'], 'body' => $antwort['body'], 'signatur' => (string) $antwort['signatur']];
|
||||
};
|
||||
|
||||
$installation = static function (string $wurzel, string $adresse, \Closure $senden): CmsZentrale {
|
||||
$db = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true]);
|
||||
$updater = new Updater($wurzel, 'sqlite:///:memory:', new CommandRunner(), $db);
|
||||
|
||||
return new CmsZentrale($wurzel, $updater, new ArchiveUpdate($wurzel, $updater, new Backup($wurzel, 'sqlite:///:memory:', $wurzel.'/var/media', $db, new CommandRunner()), new Download()), 'https://sites.example', $adresse, 'sqlite:///x', $senden);
|
||||
};
|
||||
|
||||
$site = $installation($tmp.'/site', 'https://kunde-eins.de', $ueberApi);
|
||||
$erste = $site->melden();
|
||||
$pruefe(null === $erste['auftrag'], 'Erste Meldung wird angenommen, kein Auftrag');
|
||||
$zeile = $pdo->query('SELECT * FROM installation')->fetch();
|
||||
$pruefe('kunde-eins.de' === ($zeile['domain'] ?? null) && '1.35.1' === $zeile['fassung'], 'Domain und Fassung stehen in der Zentrale');
|
||||
$ich = json_decode((string) file_get_contents($tmp.'/site/var/zentrale.json'), true);
|
||||
$pruefe(true === $ich['registriert'], 'Installation gilt danach als angemeldet');
|
||||
|
||||
$bestand = new Zentrale\Bestand($pdo);
|
||||
$pruefe(null === $bestand->beauftragen($zeile['id'], '1.36.0'), 'Update auf 1.36.0 lässt sich beauftragen');
|
||||
$zweite = $site->melden();
|
||||
$pruefe('1.36.0' === ($zweite['auftrag']['fassung'] ?? null) && 'update' === $zweite['auftrag']['aktion'], 'Folgemeldung holt den Auftrag ab');
|
||||
$pruefe(null === $site->melden()['auftrag'], 'Ein abgeholter Auftrag kommt nicht zweimal');
|
||||
$pruefe(null !== $bestand->beauftragen($zeile['id'], '1.36.0'), 'Während ein Auftrag läuft, gibt es keinen zweiten');
|
||||
|
||||
$site->auftragAngenommen($zweite['auftrag']['id'], '1.36.0');
|
||||
file_put_contents($tmp.'/site/VERSION', "1.36.0\n");
|
||||
$site->auftragErledigt(true, 'Fassung 1.36.0 läuft.');
|
||||
$site->melden();
|
||||
$auftrag = $pdo->query('SELECT * FROM auftrag')->fetch();
|
||||
$pruefe(1 === (int) $auftrag['ok'] && null !== $auftrag['erledigt'], 'Ergebnis kommt in der Zentrale an');
|
||||
$pruefe('1.36.0' === $pdo->query('SELECT fassung FROM installation')->fetchColumn(), 'Neue Fassung ist gemeldet');
|
||||
$pruefe(null === $site->offenerAuftrag(), 'Die Installation vergisst den quittierten Auftrag');
|
||||
|
||||
// Eine fremde Antwort: anderer Schlüssel unterschreibt.
|
||||
$gefaelscht = $installation($tmp.'/site', 'https://kunde-eins.de', static function (string $url, string $body) use ($ueberApi): array {
|
||||
$echt = $ueberApi($url, $body, []);
|
||||
|
||||
return [...$echt, 'signatur' => hash_hmac('sha256', $echt['body'], str_repeat('b', 64))];
|
||||
});
|
||||
try {
|
||||
$gefaelscht->melden();
|
||||
$pruefe(false, 'Unsignierte Meldung wird abgewiesen');
|
||||
} catch (\RuntimeException $e) {
|
||||
$pruefe(str_contains($e->getMessage(), '403'), 'Meldung ohne gültige Unterschrift weist die Zentrale mit 403 ab');
|
||||
}
|
||||
|
||||
// Jemand übernimmt die Kennung mit eigenem Schlüssel.
|
||||
$body = json_encode(['id' => $zeile['id'], 'adresse' => 'https://fremd.de', 'fassung' => '1.36.0', 'schluessel' => str_repeat('c', 64)]);
|
||||
$fremd = $api->melden($body, hash_hmac('sha256', $body, str_repeat('c', 64)), '198.51.100.1');
|
||||
$pruefe(403 === $fremd['status'], 'Fremder Schlüssel für eine bekannte Kennung wird abgewiesen');
|
||||
|
||||
// Die Zentrale hat die Installation vergessen: Sie meldet sich mit Schlüssel neu an.
|
||||
$pdo->exec('DELETE FROM installation');
|
||||
$site->melden();
|
||||
$pruefe(1 === (int) $pdo->query('SELECT COUNT(*) FROM installation')->fetchColumn(), 'Nach Verlust in der Zentrale meldet sich die Installation neu an');
|
||||
|
||||
// Umzug: dieselbe Kennung, andere Domain.
|
||||
$umzug = $installation($tmp.'/site', 'https://www.kunde-zwei.de', $ueberApi);
|
||||
$umzug->melden();
|
||||
$neu = $pdo->query('SELECT domain, domain_vorher FROM installation')->fetch();
|
||||
$pruefe('www.kunde-zwei.de' === $neu['domain'] && 'kunde-eins.de' === $neu['domain_vorher'], 'Domainwechsel ist sichtbar');
|
||||
|
||||
echo $fehler > 0 ? "\n{$fehler} Prüfung(en) fehlgeschlagen.\n" : "\nAlle Prüfungen bestanden.\n";
|
||||
exit($fehler > 0 ? 1 : 0);
|
||||
Reference in new issue
Block a user