168 lines
11 KiB
PHP
168 lines
11 KiB
PHP
<?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;
|
||
} |