Add a KantTresor tab to the central registry
KantTresor instances (from 1.82.0) report to /api/kanttresor/melden with the same key-then-HMAC protocol. The new tab lists them with tenants, disk usage and failing services; it never offers or accepts an update order, since KantTresor is updated with ./update.sh on the server. See ADR 124. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9d80af3795
commit
968d19b611
15 files changed
+392
-80
No files matched your search
+117
-36
@@ -11,6 +11,10 @@ namespace Zentrale;
|
||||
* Schlüssel mit; danach muss jede Meldung damit unterschrieben sein (HMAC-SHA256 über den
|
||||
* Rohtext, Kopfzeile X-KantSites-Signatur). Jede Antwort ist ebenso unterschrieben, damit die
|
||||
* Installation nur Aufträge von hier annimmt.
|
||||
*
|
||||
* POST /api/kanttresor/melden nimmt auf dieselbe Weise die Meldung einer KantTresor-Instanz an
|
||||
* (ADR 124) - mit eigenen Angaben und nie mit einem Auftrag: KantTresor wird auf dem Server
|
||||
* mit ./update.sh aktualisiert.
|
||||
*/
|
||||
final class Api
|
||||
{
|
||||
@@ -36,36 +40,17 @@ final class Api
|
||||
if (is_string($pruef)) {
|
||||
return $this->fehler(400, $pruef);
|
||||
}
|
||||
$signatur = strtolower(trim($signatur));
|
||||
|
||||
$gespeichert = $this->db->prepare('SELECT * FROM installation WHERE id = ?');
|
||||
$gespeichert->execute([$daten['id']]);
|
||||
$installation = $gespeichert->fetch() ?: null;
|
||||
$mitgebracht = is_string($daten['schluessel'] ?? null) && 1 === preg_match('/^[a-f0-9]{64}$/D', $daten['schluessel']) ? $daten['schluessel'] : null;
|
||||
|
||||
if (null === $installation) {
|
||||
if (null === $mitgebracht) {
|
||||
// Die Installation hält sich für angemeldet, hier ist sie unbekannt: Sie schickt
|
||||
// daraufhin ihren Schlüssel noch einmal.
|
||||
return $this->fehler(409, 'Diese Installation ist hier nicht bekannt.');
|
||||
}
|
||||
$schluessel = $mitgebracht;
|
||||
} else {
|
||||
$schluessel = (string) $installation['schluessel'];
|
||||
if (null !== $mitgebracht && !hash_equals($schluessel, $mitgebracht)) {
|
||||
return $this->fehler(403, 'Diese Kennung gehört zu einer anderen Installation.');
|
||||
}
|
||||
}
|
||||
if (!hash_equals(hash_hmac('sha256', $body, $schluessel), $signatur)) {
|
||||
return $this->fehler(403, 'Die Unterschrift stimmt nicht.');
|
||||
$anmeldung = $this->anmeldung($daten, $body, $signatur, 'kantsites');
|
||||
if (isset($anmeldung['status'])) {
|
||||
return $anmeldung;
|
||||
}
|
||||
['installation' => $installation, 'schluessel' => $schluessel] = $anmeldung;
|
||||
|
||||
$jetzt = Datenbank::jetzt();
|
||||
$domain = strtolower((string) parse_url($daten['adresse'], PHP_URL_HOST));
|
||||
$update = is_array($daten['update'] ?? null) ? $daten['update'] : [];
|
||||
$werte = [
|
||||
'adresse' => $daten['adresse'],
|
||||
'domain' => $domain,
|
||||
'domain' => strtolower((string) parse_url($daten['adresse'], PHP_URL_HOST)),
|
||||
'fassung' => $daten['fassung'],
|
||||
'php' => mb_substr((string) ($daten['php'] ?? ''), 0, 20),
|
||||
'datenbank' => mb_substr((string) ($daten['datenbank'] ?? ''), 0, 20),
|
||||
@@ -80,18 +65,7 @@ final class Api
|
||||
|
||||
$this->db->beginTransaction();
|
||||
try {
|
||||
if (null === $installation) {
|
||||
$sql = 'INSERT INTO installation (id, schluessel, erste_meldung, '.implode(', ', array_keys($werte)).') VALUES (?, ?, ?, '.implode(', ', array_fill(0, count($werte), '?')).')';
|
||||
$this->db->prepare($sql)->execute([$daten['id'], $schluessel, $jetzt, ...array_values($werte)]);
|
||||
} else {
|
||||
// Meldet sich dieselbe Kennung unter einer anderen Domain, wurde die Installation
|
||||
// kopiert oder umgezogen. Beides will man sehen.
|
||||
if ($installation['domain'] !== $domain) {
|
||||
$werte['domain_vorher'] = $installation['domain'];
|
||||
}
|
||||
$sql = 'UPDATE installation SET '.implode(', ', array_map(static fn (string $spalte): string => $spalte.' = ?', array_keys($werte))).' WHERE id = ?';
|
||||
$this->db->prepare($sql)->execute([...array_values($werte), $daten['id']]);
|
||||
}
|
||||
$this->speichern($daten['id'], $schluessel, $installation, $werte);
|
||||
|
||||
$ergebnis = $daten['auftrag'] ?? null;
|
||||
if (is_array($ergebnis) && is_int($ergebnis['id'] ?? null) && is_bool($ergebnis['ok'] ?? null)) {
|
||||
@@ -117,6 +91,113 @@ final class Api
|
||||
return ['status' => 200, 'body' => $antwort, 'signatur' => hash_hmac('sha256', $antwort, $schluessel)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Meldung einer KantTresor-Instanz: Stand, Mandantenzahl, gestörte Dienste, Speicher.
|
||||
* Die Antwort enthält nie einen Auftrag.
|
||||
*
|
||||
* @return array{status: int, body: string, signatur: ?string}
|
||||
*/
|
||||
public function meldenTresor(string $body, string $signatur, string $ip): array
|
||||
{
|
||||
if (strlen($body) > self::GROESSTE_MELDUNG) {
|
||||
return $this->fehler(413, 'Die Meldung ist zu groß.');
|
||||
}
|
||||
$daten = json_decode($body, true);
|
||||
if (!is_array($daten)) {
|
||||
return $this->fehler(400, 'Die Meldung ist kein JSON.');
|
||||
}
|
||||
$pruef = $this->pruefen($daten);
|
||||
if (is_string($pruef)) {
|
||||
return $this->fehler(400, $pruef);
|
||||
}
|
||||
$anmeldung = $this->anmeldung($daten, $body, $signatur, 'kanttresor');
|
||||
if (isset($anmeldung['status'])) {
|
||||
return $anmeldung;
|
||||
}
|
||||
['installation' => $installation, 'schluessel' => $schluessel] = $anmeldung;
|
||||
|
||||
$stoerungen = is_array($daten['stoerungen'] ?? null) ? array_slice(array_values(array_filter($daten['stoerungen'], 'is_string')), 0, 10) : [];
|
||||
$mandanten = $daten['mandanten'] ?? null;
|
||||
$speicher = $daten['speicher'] ?? null;
|
||||
$werte = [
|
||||
'produkt' => 'kanttresor',
|
||||
'adresse' => $daten['adresse'],
|
||||
'domain' => strtolower((string) parse_url($daten['adresse'], PHP_URL_HOST)),
|
||||
'fassung' => $daten['fassung'],
|
||||
'laufzeit' => mb_substr((string) ($daten['laufzeit'] ?? ''), 0, 60),
|
||||
'datenbank' => mb_substr((string) ($daten['datenbank'] ?? ''), 0, 20),
|
||||
'mandanten' => is_int($mandanten) && $mandanten >= 0 && $mandanten <= 100000 ? $mandanten : null,
|
||||
'stoerungen' => mb_substr(implode(', ', array_map(static fn (string $s): string => mb_substr(trim($s), 0, 60), $stoerungen)), 0, 300),
|
||||
'speicher' => is_int($speicher) && $speicher >= 0 && $speicher <= 100 ? $speicher : null,
|
||||
'ip' => mb_substr($ip, 0, 45),
|
||||
'letzte_meldung' => Datenbank::jetzt(),
|
||||
];
|
||||
$this->speichern($daten['id'], $schluessel, $installation, $werte);
|
||||
|
||||
$antwort = '{"ok":true}';
|
||||
|
||||
return ['status' => 200, 'body' => $antwort, 'signatur' => hash_hmac('sha256', $antwort, $schluessel)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wer meldet sich? Beim ersten Kontakt mit Schlüssel, danach nur mit gültiger Unterschrift.
|
||||
*
|
||||
* @param array<string, mixed> $daten
|
||||
*
|
||||
* @return array{installation: ?array<string, mixed>, schluessel: string}|array{status: int, body: string, signatur: null}
|
||||
*/
|
||||
private function anmeldung(array $daten, string $body, string $signatur, string $produkt): array
|
||||
{
|
||||
$signatur = strtolower(trim($signatur));
|
||||
$gespeichert = $this->db->prepare('SELECT * FROM installation WHERE id = ?');
|
||||
$gespeichert->execute([$daten['id']]);
|
||||
$installation = $gespeichert->fetch() ?: null;
|
||||
$mitgebracht = is_string($daten['schluessel'] ?? null) && 1 === preg_match('/^[a-f0-9]{64}$/D', $daten['schluessel']) ? $daten['schluessel'] : null;
|
||||
|
||||
if (null === $installation) {
|
||||
if (null === $mitgebracht) {
|
||||
// Die Installation hält sich für angemeldet, hier ist sie unbekannt: Sie schickt
|
||||
// daraufhin ihren Schlüssel noch einmal.
|
||||
return $this->fehler(409, 'Diese Installation ist hier nicht bekannt.');
|
||||
}
|
||||
$schluessel = $mitgebracht;
|
||||
} else {
|
||||
$schluessel = (string) $installation['schluessel'];
|
||||
if (null !== $mitgebracht && !hash_equals($schluessel, $mitgebracht)) {
|
||||
return $this->fehler(403, 'Diese Kennung gehört zu einer anderen Installation.');
|
||||
}
|
||||
}
|
||||
if (!hash_equals(hash_hmac('sha256', $body, $schluessel), $signatur)) {
|
||||
return $this->fehler(403, 'Die Unterschrift stimmt nicht.');
|
||||
}
|
||||
if (null !== $installation && $produkt !== $installation['produkt']) {
|
||||
return $this->fehler(403, 'Diese Kennung gehört zu einem anderen Produkt.');
|
||||
}
|
||||
|
||||
return ['installation' => $installation, 'schluessel' => $schluessel];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $installation
|
||||
* @param array<string, mixed> $werte mit letzte_meldung
|
||||
*/
|
||||
private function speichern(string $id, string $schluessel, ?array $installation, array $werte): void
|
||||
{
|
||||
if (null === $installation) {
|
||||
$sql = 'INSERT INTO installation (id, schluessel, erste_meldung, '.implode(', ', array_keys($werte)).') VALUES (?, ?, ?, '.implode(', ', array_fill(0, count($werte), '?')).')';
|
||||
$this->db->prepare($sql)->execute([$id, $schluessel, $werte['letzte_meldung'], ...array_values($werte)]);
|
||||
|
||||
return;
|
||||
}
|
||||
// Meldet sich dieselbe Kennung unter einer anderen Domain, wurde die Installation
|
||||
// kopiert oder umgezogen. Beides will man sehen.
|
||||
if ($installation['domain'] !== $werte['domain']) {
|
||||
$werte['domain_vorher'] = $installation['domain'];
|
||||
}
|
||||
$sql = 'UPDATE installation SET '.implode(', ', array_map(static fn (string $spalte): string => $spalte.' = ?', array_keys($werte))).' WHERE id = ?';
|
||||
$this->db->prepare($sql)->execute([...array_values($werte), $id]);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $daten */
|
||||
private function pruefen(array $daten): ?string
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user