Files
deskhub/Plugins/plugins/ueberwachung/meldung.php
T
2026-10-24 20:15:16 +00:00

168 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// Texte der Überwachung: lesbare Fehler und der Aufbau der Tickets und Mails. Alle Funktionen beginnen mit ueberwachung_.
/** Rohe PHP-Fehlermeldung in verständliches Deutsch übersetzen. Beginnt mit „Nicht erreichbar:“ oder „Zertifikat/TLS-Fehler:“. */
function ueberwachung_describe_error(string $raw): string
{
$r = strtolower($raw);
if (preg_match('/getaddrinfo|php_network_getaddresses|name or service not known|nodename nor servname|temporary failure in name resolution|no address associated|could not resolve host/', $r)) return 'Nicht erreichbar: Der Name der Adresse lässt sich nicht auflösen (DNS)';
if (str_contains($r, 'connection refused')) return 'Nicht erreichbar: Verbindung abgelehnt (der Dienst läuft nicht oder der Port ist gesperrt)';
if (preg_match('/timed out|timeout|zeitüberschreitung/', $r)) return 'Nicht erreichbar: Zeitüberschreitung (keine Antwort vom Server)';
if (preg_match('/no route to host|network is unreachable/', $r)) return 'Nicht erreichbar: Der Server ist aus dem Netz nicht erreichbar';
if (preg_match('/certificate|ssl|tls|handshake/', $r)) {
$why = preg_match('/expired/', $r) ? ' (Zertifikat abgelaufen)' : (preg_match('/verify failed|self.signed|unable to get local issuer|certificate problem|does not match|no alternative certificate subject/', $r) ? ' (Zertifikat ungültig oder nicht vertrauenswürdig)' : '');
return 'Zertifikat/TLS-Fehler' . $why . ': Die gesicherte Verbindung kam nicht zustande';
}
return 'Nicht erreichbar: Die Verbindung kam nicht zustande (Dienst aus, Port zu oder Problem mit der gesicherten Verbindung)';
}
function ueberwachung_host(string $target): string
{
$h = parse_url($target, PHP_URL_HOST);
return (string) ($h ?: preg_replace('/:\d+$/', '', $target));
}
/** Betreff mit Name und Adresse (die Adresse entfällt, wenn der Name sie schon enthält). */
function ueberwachung_subject(string $prefix, array $m): string
{
$host = ueberwachung_host((string) $m['target']);
return $prefix . ': ' . $m['name'] . (stripos((string) $m['name'], $host) === false ? ' (' . $host . ')' : '');
}
function ueberwachung_type_label(string $type): string
{
return ueberwachung_types()[$type] ?? $type;
}
/** Zweispaltige Tabelle: Bezeichnung → Wert (beides wird maskiert). @param list<array{0:string,1:string}> $rows */
function ueberwachung_table(array $rows): string
{
$html = '<table border="0" cellpadding="5" cellspacing="0">';
foreach ($rows as [$k, $v]) $html .= '<tr><td><strong>' . e($k) . '</strong></td><td>' . e($v) . '</td></tr>';
return $html . '</table>';
}
function ueberwachung_dt(?string $dt): string
{
return $dt ? date('d.m.Y H:i:s', (int) strtotime($dt)) : '–';
}
/** Zeilen zu einem Zertifikat. @return list<array{0:string,1:string}> */
function ueberwachung_cert_rows(array $m, array $res, ?int $days = null): array
{
if (empty($res['cert_expires'])) return [];
$days ??= (int) floor(((int) $res['cert_expires'] - time()) / 86400);
$rows = [['Zertifikat gültig bis', date('d.m.Y H:i', (int) $res['cert_expires']) . ($days < 0 ? ' (seit ' . abs($days) . ' Tagen abgelaufen)' : ' (noch ' . $days . ' Tage)')]];
if (!empty($res['issuer'])) $rows[] = ['Aussteller', (string) $res['issuer']];
if (!empty($res['cert_subject'])) $rows[] = ['Ausgestellt für', (string) $res['cert_subject']];
return $rows;
}
/** Hinweise zur Fehlersuche je nach Art und Fehler. @return list<string> */
function ueberwachung_hints(array $m, string $error): array
{
$e = strtolower($error);
if (str_starts_with($error, 'Zertifikat')) return ['Zertifikat prüfen: abgelaufen, falscher Name oder fehlende Zwischenzertifikate?', 'Zertifikat erneuern (zum Beispiel Let’s Encrypt in Plesk) und die Seite im Browser öffnen.'];
if (preg_match('/http-status 5\d\d/', $e)) return ['Der Server antwortet, aber die Anwendung meldet einen Fehler: Fehlerprotokoll des Webservers und der Anwendung ansehen.', 'Datenbank, Speicherplatz und PHP-Prozess prüfen.', 'Kürzlich eingespielte Änderungen oder Updates zurücknehmen.'];
if (preg_match('/http-status 4\d\d/', $e)) return ['Die Adresse antwortet mit einem Fehler (zum Beispiel 404 oder 403): Adresse, Weiterleitung und Zugriffsschutz prüfen.'];
if (str_contains($e, 'standard- oder fehlerseite')) return ['Der Server liefert nur seine Standard- oder Fehlerseite aus: Ist der Eintrag im Reverse-Proxy (zum Beispiel Nginx Proxy Manager) aktiviert, und läuft der Dienst dahinter?', 'Adresse und Port des Ziels im Proxy prüfen und den Dienst dahinter ansehen.'];
if (str_contains($e, 'text fehlt')) return ['Die Seite antwortet, enthält aber nicht den erwarteten Text: Wartungsseite, Fehlerseite oder veränderter Inhalt?'];
if (str_contains($e, 'dns')) return ['DNS-Eintrag der Adresse prüfen (Domain abgelaufen? Eintrag geändert?).'];
$hints = ['Läuft der Server und der Dienst? Neustart des Webservers bzw. Dienstes versuchen.', 'Firewall und Port-Freigaben prüfen.'];
if ($m['type'] === 'http') $hints[] = 'Die Seite einmal im Browser und von einem anderen Netz aus öffnen, um ein lokales Problem auszuschließen.';
return $hints;
}
function ueberwachung_hint_list(array $hints): string
{
return '<h3>Mögliche Ursachen und nächste Schritte</h3><ul>' . implode('', array_map(fn($h) => '<li>' . e($h) . '</li>', $hints)) . '</ul>';
}
/** Text des Ausfall-Tickets. */
function ueberwachung_down_body(array $m, array $res, int $fails, ?string $firstFail): string
{
$rows = [
['Dienst', (string) $m['name']],
['Art der Prüfung', ueberwachung_type_label((string) $m['type'])],
['Adresse', (string) $m['target']],
['Zustand', 'Ausfall'],
['Fehler', (string) $res['error']],
];
if (!empty($res['status'])) $rows[] = ['HTTP-Status', (string) $res['status']];
$rows[] = ['Seit', ueberwachung_dt($firstFail) . ($firstFail ? ' (vor ' . ueberwachung_duration(max(0, time() - (int) strtotime($firstFail))) . ')' : '')];
$rows[] = ['Fehlgeschlagene Prüfungen in Folge', $fails . ' (Prüfung alle ' . (int) $m['interval_min'] . ' Minuten, Ausfall ab ' . ueberwachung_config()['threshold'] . ' in Folge)'];
$rows[] = ['Letzter erfolgreicher Abruf', $m['last_ok_at'] ? ueberwachung_dt((string) $m['last_ok_at']) : 'noch nie'];
$rows[] = ['Dauer der letzten Prüfung', (int) $res['ms'] . ' ms (Zeitlimit ' . (int) $m['timeout_sec'] . ' Sekunden)'];
if (!empty($m['expect_text'])) $rows[] = ['Erwarteter Text', (string) $m['expect_text']];
foreach (ueberwachung_cert_rows($m, $res) as $r) $rows[] = $r;
$rows[] = ['Geprüft von', (string) (gethostname() ?: 'DeskHub-Server') . ', ' . date('d.m.Y H:i:s')];
return '<p><strong>' . e($m['name']) . '</strong> ist nicht erreichbar bzw. fehlerhaft.</p>' . ueberwachung_table($rows)
. ueberwachung_hint_list(ueberwachung_hints($m, (string) $res['error']))
. '<p><em>Diese Meldung stammt von der automatischen Überwachung. Sobald der Dienst wieder erreichbar ist, wird das Ticket automatisch gelöst.</em></p>';
}
/** Text des Zertifikat-Tickets. */
function ueberwachung_cert_body(array $m, array $res, int $days): string
{
$headline = $days < 0 ? 'Das Zertifikat ist seit ' . abs($days) . ' Tagen abgelaufen.' : 'Das Zertifikat läuft in ' . $days . ' Tagen ab.';
$rows = array_merge([
['Dienst', (string) $m['name']],
['Art der Prüfung', ueberwachung_type_label((string) $m['type'])],
['Adresse', (string) $m['target']],
], ueberwachung_cert_rows($m, $res, $days), [
['Warnung ab', (int) $m['cert_warn_days'] . ' Tagen vor Ablauf'],
['Geprüft von', (string) (gethostname() ?: 'DeskHub-Server') . ', ' . date('d.m.Y H:i:s')],
]);
return '<p><strong>' . e($headline) . '</strong></p>' . ueberwachung_table($rows)
. ueberwachung_hint_list(['Zertifikat erneuern (zum Beispiel Let’s Encrypt in Plesk oder beim Anbieter).', 'Nach der Erneuerung die Seite im Browser öffnen und die Kette prüfen.', 'Bei automatischer Erneuerung: prüfen, warum sie nicht gelaufen ist.'])
. '<p><em>Nach der Erneuerung wird dieses Ticket automatisch gelöst.</em></p>';
}
/** Interne Notiz bei Entwarnung (Ausfall). */
function ueberwachung_up_note(array $m, string $dur): string
{
$since = $m['first_fail_at'] ?? $m['down_since'] ?? null;
return 'Wieder erreichbar (' . $m['name'] . '). Der Ausfall dauerte ' . $dur . '.'
. ($since ? ' Von ' . ueberwachung_dt((string) $since) . ' bis ' . date('d.m.Y H:i:s') . '.' : '');
}
/** Titel der Seite (für Anzeige und die Erkennung von Standard- und Fehlerseiten). */
function ueberwachung_page_title(string $body): string
{
if (!preg_match('/<title[^>]*>(.*?)<\/title>/is', $body, $m)) return '';
return mb_substr(trim((string) preg_replace('/\s+/u', ' ', html_entity_decode(strip_tags($m[1]), ENT_QUOTES | ENT_HTML5, 'UTF-8'))), 0, 150);
}
/**
* Antwortet ein Server mit 200, liefert aber nur seine Standard- oder Fehlerseite (zum Beispiel ein abgeschalteter Eintrag im Proxy), ist das
* Ziel trotzdem ausgefallen. Erkannt wird das am Seitentitel. Mit „erwartetem Text“ am Ziel entfällt diese Prüfung. @return string|null Fehlertext
*/
function ueberwachung_default_page(string $title, string $body = ''): ?string
{
// Titel bekannter Standard- und Fehlerseiten (Nginx Proxy Manager: „Default Site“)
if ($title !== '' && preg_match('/^default site$|congratulations|welcome to nginx|nginx.*default|default (web )?(server )?page|test page for|apache2? .*default page|^it works!?$|^index of \/|iis windows server|^welcome to openresty|bad gateway|service (temporarily )?unavailable|gateway time-?out|^50[234]\b|^404\b|not found$/i', $title)) {
return 'Der Server antwortet mit einer Standard- oder Fehlerseite statt mit der Anwendung (Seitentitel: „' . mb_substr($title, 0, 80) . '“)';
}
// Typische Sätze der Standardseiten, auch wenn der Titel nichtssagend ist
if ($body !== '' && preg_match('/successfully started the nginx proxy manager|trying to access a host that isn.?t set up yet|if you see this page, the nginx web server is successfully installed|apache2 (ubuntu|debian) default page|this is the default welcome page|web server\'s default page|domain (is )?(not configured|parked)/i', $body, $m)) {
return 'Der Server antwortet mit einer Standardseite statt mit der Anwendung (Text auf der Seite: „' . mb_substr($m[0], 0, 80) . '“)';
}
return null;
}
/**
* Soll die Warnung „PHP kennt keine vertrauenswürdigen Zertifizierungsstellen“ erscheinen? Nur wenn das System es annimmt UND es auch so aussieht:
* Mindestens ein HTTPS-Ziel mit Zertifikatsprüfung scheitert an der Zertifikatsprüfung und keines wurde erfolgreich geprüft. Wird irgendeine
* HTTPS-Seite mit Prüfung erreicht, funktioniert die Prüfung, egal was die Dateisuche meint.
* @param list<array<string,mixed>> $monitors
*/
function ueberwachung_ca_warning(array $monitors, bool $systemSaysMissing): bool
{
if (!$systemSaysMissing) return false;
$bad = 0;
$good = 0;
foreach ($monitors as $m) {
if (($m['type'] ?? '') !== 'http' || !str_starts_with((string) $m['target'], 'https') || (int) ($m['verify_tls'] ?? 1) !== 1 || empty($m['last_check_at'])) continue;
if (str_starts_with((string) ($m['last_error'] ?? ''), 'Zertifikat/TLS-Fehler')) $bad++; else $good++;
}
return $bad > 0 && $good === 0;
}