From 087f37afa60781ecc2989d7c04316b6a4c2ac184 Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Wed, 19 Aug 2026 18:29:51 +0200 Subject: [PATCH] 1.3.0: PDF-Zweig SIP-Erreichbarkeit, Token in der URL nur noch fuer den APK-Download [deploy] PDF-Zweig netdiagPdfSip() - Eigener Zweig, weil die Aussage sonst untergeht: in einer flachen Schluessel/Wert-Liste sieht "401 Unauthorized" wie ein Fehler aus, ist aber der Beweis fuer eine erreichbare Instanz, die nur Zugangsdaten will. Erst Befund im Klartext, dann eine Zeile je Transportweg. - Der Grenzhinweis steht mit im Dokument: gemessen ist die Erreichbarkeit, NICHT die Sprachqualitaet. - Feldnamen-Falle wieder aufgetreten: 'port' gehoert in beiden Feldtabellen laengst dem SNMP-Werkzeug ("Switch-Port"). Im Kunden-PDF haette gestanden "Switch-Port: 5060" - deshalb 'zielPort'. WLAN-Kanalanalyse: Warnungen nicht mehr abschneiden - dol_trunc(..., 160) kappte genau die Begruendung ("... In dicht besiedelter Umgebung meist ein Fehle..."). Eine Warnung ohne ihren Grund ist im Kundendokument wertlos; MultiCell bricht ohnehin um. Token in der Adresszeile - netdiag_api_read_token() nimmt ?jwt= nur noch an, wenn der Endpunkt es ausdruecklich erlaubt. Einzige Stelle: update.php?download=1. - Grund: ein Token in der URL steht in jedem Zugriffs- und Proxy-Log und gilt sieben Tage fuer die GESAMTE Kunden-API. - Warum die Ausnahme bleibt: die App-Fassungen im Feld bauen die Download-Adresse mit dem Token darin. Sofort schliessen hiesse, genau die Geraete vom Update auszusperren, die die neue APK brauchen. Zu entfernen, sobald Eddy den Rollout bestaetigt (Hinweis steht im Code). - pdf.php nimmt ab sofort ausschliesslich den Authorization-Header. Gegen die Testinstanz gemessen: ?jwt= liefert bei orders/customers/protocols/ pdf/update-Version jetzt 401, mit Bearer weiterhin 200. Co-Authored-By: Claude Opus 5 (1M context) --- ChangeLog.md | 51 +++++++++++++ api/netdiag_api.lib.php | 30 ++++++-- api/pdf.php | 6 +- api/update.php | 13 +++- core/modules/modNetDiag.class.php | 2 +- lib/netdiag.lib.php | 17 +++++ lib/netdiag_pdf.lib.php | 123 +++++++++++++++++++++++++++++- 7 files changed, 230 insertions(+), 12 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index c7b413d..5afe215 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,56 @@ # Changelog NetDiag +## 1.3.0 — 2026-08-19 + +### Neues Werkzeug: SIP-Erreichbarkeit (Telefonie) + +Die App fragt Telefonanlage oder Provider mit einer OPTIONS-Anfrage (RFC 3261) — +ohne Anruf, ohne Zugangsdaten, ohne dass irgendwo ein Telefon klingelt. Modulseitig +kommt der PDF-Zweig dazu. + +- `netdiagPdfSip()` in `lib/netdiag_pdf.lib.php`: Ziel, Port und Statuszeile in der + Kopfzeile, darunter der Befund im Klartext, dann eine Zeile je Transportweg. + **Warum ein eigener Zweig:** in einer flachen Schlüssel/Wert-Liste sieht „401 + Unauthorized" wie ein Fehler aus. Tatsächlich ist es der Beweis, dass die Instanz + erreichbar ist und antwortet — sie will nur Zugangsdaten sehen. Diese Unterscheidung + ist im Kundendokument der Unterschied zwischen „Anlage tot" und „Zugangsdaten prüfen". +- Der Grenzhinweis steht mit im PDF: gemessen ist die **Erreichbarkeit**, NICHT die + Sprachqualität. RTP-Jitter ohne aufgebautes Gespräch zu behaupten wäre eine + Falschaussage. +- `netdiagToolName()`: `sipcheck` → „SIP-Erreichbarkeit". +- `netdiagKundenfelder()` um die Felder des Werkzeugs ergänzt (`befund`, + `statuszeile`, `antwortWeg`, `anmeldungNoetig`, `versuche`, `unterstuetzteMethoden`, + `zielIp`, `zielHerkunft`, `zielPort`, `laufzeitMs`). + **Achtung, dabei aufgefallen:** der naheliegende Schlüssel `port` war bereits vom + SNMP-Werkzeug als „Switch-Port" belegt. Ein SIP-Port unter diesem Namen stünde im + Kundendokument falsch beschriftet — deshalb `zielPort`. + +### WLAN-Kanalanalyse: Warnungen werden nicht mehr abgeschnitten + +`netdiagPdfWifiKanal()` kürzte jede Warnung nach 160 Zeichen (`dol_trunc`). Betroffen +war genau die Begründung: „… blockiert damit praktisch 2 der 3 störungsfreien Kanäle. +In dicht besiedelter Umgebung meist ein Fehle…". Eine Warnung ohne ihren Grund ist im +Kundendokument wertlos; MultiCell bricht ohnehin sauber um. + +### Sicherheit: Token in der Adresszeile nur noch für den APK-Download + +`netdiag_api_read_token()` nimmt `?jwt=` nicht mehr allgemein an, sondern nur noch, +wenn der Endpunkt es ausdrücklich erlaubt (`netdiag_api_authenticate($db, true)`). +Einzige Stelle: `update.php?download=1`. + +Warum überhaupt: ein Token in der URL landet in Zugriffs- und Proxy-Logs und gilt +sieben Tage für die **gesamte** Kunden-API. Vorher hätte damit jeder, der ein Log +liest, Aufträge, Kunden und Protokolle abrufen können. + +Warum die eine Ausnahme bleibt: die im Feld installierten App-Fassungen bauen die +Download-Adresse mit dem Token darin. Wird der Zweig sofort geschlossen, kommt kein +altes Gerät mehr an die neue APK — und die neue APK ist genau die, die den Zweig +überflüssig macht. **Zu entfernen, sobald der Rollout bestätigt ist** (Hinweis steht +im Code an beiden Stellen). + +`pdf.php` nimmt ab sofort ausschließlich den `Authorization: Bearer`-Header. + + ## 1.2.0 — 2026-08-17 ### Anmeldung nur noch über AWL-Auth — der eigene JWT-Pfad ist weg diff --git a/api/netdiag_api.lib.php b/api/netdiag_api.lib.php index 692c348..b1e8a66 100644 --- a/api/netdiag_api.lib.php +++ b/api/netdiag_api.lib.php @@ -198,11 +198,24 @@ function netdiag_api_error($message, $httpstatus = 400) /** - * Token aus Request lesen (Authorization-Header oder ?jwt=). + * Token aus Request lesen. * - * @return string Token oder leerer String + * Regelfall ist ausschliesslich der `Authorization: Bearer`-Header. Ein Token in + * der Adresszeile landet in Zugriffs- und Proxy-Logs und im Verlauf jedes + * Zwischensystems — bei einem Langzeit-Token (Standard 7 Tage) ist das ein + * Nachschluessel zur gesamten Kunden-API. + * + * Genau eine Ausnahme, bewusst und befristet: der APK-Download in `update.php`. + * Die im Feld installierten Fassungen rufen ihn mit `?jwt=` auf. Wer den Zweig + * jetzt schon entfernt, sperrt diese Geraete vom Update aus — und die einzige + * Quelle der neuen APK ist genau dieser Endpunkt. Deshalb bleibt er, bis Eddy + * den Rollout bestaetigt; er gilt dann nur noch fuer eine einzige, lesende + * Datei-Auslieferung statt fuer die ganze API. + * + * @param bool $allowQuery Query-Parameter `?jwt=` zusaetzlich zulassen + * @return string Token oder leerer String */ -function netdiag_api_read_token() +function netdiag_api_read_token($allowQuery = false) { $auth = ''; if (!empty($_SERVER['HTTP_AUTHORIZATION'])) { @@ -218,7 +231,7 @@ function netdiag_api_read_token() if (stripos($auth, 'Bearer ') === 0) { return trim(substr($auth, 7)); } - if (!empty($_GET['jwt'])) { + if ($allowQuery && !empty($_GET['jwt'])) { return (string) $_GET['jwt']; } return ''; @@ -233,12 +246,13 @@ function netdiag_api_read_token() * damit die Umstellung niemanden mitten im Einsatz aussperrt. Er entfällt, * sobald alle Geräte einmal neu angemeldet sind (siehe ROADMAP_UMSETZUNG L5). * - * @param DoliDB $db Datenbank-Handler - * @return User Geladenes Benutzer-Objekt + * @param DoliDB $db Datenbank-Handler + * @param bool $allowQuery Token auch aus `?jwt=` annehmen (nur APK-Download) + * @return User Geladenes Benutzer-Objekt */ -function netdiag_api_authenticate($db) +function netdiag_api_authenticate($db, $allowQuery = false) { - $token = netdiag_api_read_token(); + $token = netdiag_api_read_token($allowQuery); if (empty($token)) { netdiag_api_error('Kein Token übermittelt', 401); } diff --git a/api/pdf.php b/api/pdf.php index 742877d..4ca18c0 100644 --- a/api/pdf.php +++ b/api/pdf.php @@ -18,7 +18,11 @@ /** * \file netdiag/api/pdf.php * \ingroup netdiag - * \brief API-Endpunkt: Protokoll-PDF streamen (GET ?id=&jwt=). + * \brief API-Endpunkt: Protokoll-PDF streamen (GET ?id=). + * + * Authentifizierung ausschliesslich per `Authorization: Bearer`-Header. Ein + * Token in der Adresszeile stuende in jedem Zugriffs- und Proxy-Log; der + * Aufrufer holt das PDF deshalb per fetch und verarbeitet es als Blob. */ require_once __DIR__.'/netdiag_api.lib.php'; diff --git a/api/update.php b/api/update.php index a4b76a7..ed332de 100644 --- a/api/update.php +++ b/api/update.php @@ -36,7 +36,18 @@ netdiag_api_bootstrap(); * @var DoliDB $db */ -$user = netdiag_api_authenticate($db); +/* + * Einzige Stelle im Modul, die ein Token noch aus der Adresszeile annimmt + * (`?jwt=`) — und nur fuer den Datei-Download. Grund: die im Feld installierten + * App-Fassungen bauen die Download-Adresse mit dem Token darin. Wird der Zweig + * jetzt geschlossen, kommt kein altes Geraet mehr an die neue APK, die genau + * diese Abhaengigkeit aufloest. Die Versionsabfrage (ohne ?download=) laeuft + * bereits ueber den normalen fetch-Weg der App und braucht ihn nicht. + * + * Zu entfernen, sobald Eddy den Rollout der Fassung >= 20260819 bestaetigt: + * Parameter streichen -> `netdiag_api_authenticate($db)`. + */ +$user = netdiag_api_authenticate($db, isset($_GET['download'])); $registry = 'https://git.data-it-solution.de'; $owner = 'data-it'; diff --git a/core/modules/modNetDiag.class.php b/core/modules/modNetDiag.class.php index 25b2fdc..a48b390 100644 --- a/core/modules/modNetDiag.class.php +++ b/core/modules/modNetDiag.class.php @@ -67,7 +67,7 @@ class modNetDiag extends DolibarrModules $this->editor_url = ''; $this->editor_squarred_logo = ''; - $this->version = '1.2.0'; + $this->version = '1.3.0'; // Konstanten-Name in llx_const für Modul-Status $this->const_name = 'MAIN_MODULE_'.strtoupper($this->name); diff --git a/lib/netdiag.lib.php b/lib/netdiag.lib.php index 2cf51bb..8e5f263 100644 --- a/lib/netdiag.lib.php +++ b/lib/netdiag.lib.php @@ -366,6 +366,22 @@ function netdiagKundenfelder() 'mitNeuerAdresse' => array('Mit neuer IP-Adresse', ''), 'andereOffenePorts' => array('Gleiche Adresse, andere offene Ports', ''), 'unveraendert' => array('Unverändert', ''), + // SIP-Erreichbarkeit (Telefonie). Der Befund ist die eigentliche Aussage: + // „401 Anmeldung verlangt" ist ein ERREICHBARER Anschluss, nicht der + // Ausfall, als den ihn eine nackte Ampel erscheinen ließe. + 'befund' => array('Befund', ''), + 'statuszeile' => array('Antwort der Gegenstelle', ''), + 'antwortWeg' => array('Antwort über', ''), + 'anmeldungNoetig' => array('Anmeldung verlangt', ''), + 'versuche' => array('Versuche je Transportweg', ''), + 'unterstuetzteMethoden' => array('Unterstützte SIP-Methoden', ''), + 'zielIp' => array('Adresse des Ziels', ''), + 'zielHerkunft' => array('Herkunft des Ziels', ''), + // NICHT 'port' — der Schlüssel gehört hier bereits dem SNMP-Werkzeug + // („Switch-Port"); ein SIP-Port unter diesem Namen stünde falsch beschriftet + // im Kundendokument. + 'zielPort' => array('Port', ''), + 'laufzeitMs' => array('Laufzeit', 'ms'), // Netz/DHCP 'server' => array('DHCP-Server', ''), 'lease' => array('Lease-Dauer', 's'), @@ -661,6 +677,7 @@ function netdiagToolName($tool) 'wifitrack' => 'WLAN-Empfang', 'dnscheck' => 'DNS-Prüfung', 'geraetediff' => 'Geräte-Vergleich', + 'sipcheck' => 'SIP-Erreichbarkeit', // nicht mehr vorhandene Werkzeuge, aber in Altprotokollen enthalten 'dhcpcheck' => 'DHCP-Prüfung', 'wifiscan' => 'WLAN-Scan', diff --git a/lib/netdiag_pdf.lib.php b/lib/netdiag_pdf.lib.php index 8dee02c..26193a8 100644 --- a/lib/netdiag_pdf.lib.php +++ b/lib/netdiag_pdf.lib.php @@ -224,6 +224,8 @@ function netdiagGeneratePdf($db, $protocol, $outputlangs) netdiagPdfDns($pdf, $m->result); } elseif ($m->tool === 'geraetediff') { netdiagPdfGeraeteDiff($pdf, $m->result); + } elseif ($m->tool === 'sipcheck') { + netdiagPdfSip($pdf, $m->result); } else { $pdf->MultiCell(180, 5, netdiagPdfFlattenResult($m->result), 1, 'L'); } @@ -831,7 +833,15 @@ function netdiagPdfWifiKanal($pdf, $json) if (!empty($data['warnungen']) && is_array($data['warnungen'])) { foreach ($data['warnungen'] as $w) { - $pdf->MultiCell(180, 5, '! '.dol_trunc(netdiagPdfText($w), 160), 1, 'L'); + /* + * Ungekürzt. Vorher stand hier dol_trunc(..., 160) — und genau die + * Begründung fiel weg: „…blockiert damit 2 der 3 störungsfreien + * Kanäle. In dicht besiedelter Umgebung meist ein Fehle…". Die + * Warnung ohne ihren Grund ist im Kundendokument wertlos, und + * MultiCell bricht ohnehin sauber um. + */ + netdiagPdfEnsureSpace($pdf, 10); + $pdf->MultiCell(180, 5, '! '.netdiagPdfText(netdiagKundentext((string) $w)), 1, 'L'); } } @@ -861,3 +871,114 @@ function netdiagPdfWifiKanal($pdf, $json) $pdf->Cell(25, 5, (string) ($n['rssi'] ?? '').' dBm', 1, 1, 'R'); } } + +/** + * SIP-Erreichbarkeit ins PDF schreiben. + * + * Eigener Zweig, weil die entscheidende Aussage sonst untergeht: „401 + * Unauthorized" sieht in einer flachen Schlüssel/Wert-Liste wie ein Fehler + * aus, ist aber der Beweis für einen erreichbaren Anschluss. Deshalb steht + * hier der Klartext-Befund zuerst und in eigener Zeile — er ist das, was der + * Kunde und der nächste Techniker lesen. + * + * @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile) + * @param string $json result-JSON der Messung + * @return void + */ +function netdiagPdfSip($pdf, $json) +{ + $data = json_decode((string) $json, true); + if (!is_array($data)) { + $pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L'); + return; + } + + // Kopfzeile: Ziel, Port, Antwort + $sum = array(); + if (!empty($data['ziel'])) { + $teil = 'Ziel: '.$data['ziel']; + if (!empty($data['zielIp']) && $data['zielIp'] !== $data['ziel']) { + $teil .= ' ('.$data['zielIp'].')'; + } + $sum[] = $teil; + } + if (!empty($data['zielPort'])) { + $sum[] = 'Port '.$data['zielPort']; + } + if (!empty($data['statuszeile'])) { + $sum[] = 'Antwort: '.$data['statuszeile']; + } else { + $sum[] = 'Antwort: keine'; + } + if (!empty($data['antwortWeg'])) { + $sum[] = 'über '.strtoupper((string) $data['antwortWeg']); + } + if (!empty($sum)) { + $pdf->MultiCell(180, 5, netdiagPdfText(implode(' | ', $sum)), 1, 'L'); + } + + // Der Befund im Klartext — die eigentliche Aussage der Messung + if (!empty($data['befund'])) { + netdiagPdfEnsureSpace($pdf, 10); + $pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext((string) $data['befund'])), 1, 'L'); + } + + // Je Transportweg eine Zeile: erst damit ist belegbar, ob UDP still blieb + // und nur TCP antwortete (Firewall) oder ob gar nichts kam. + $versuche = (isset($data['versuche']) && is_array($data['versuche'])) ? $data['versuche'] : array(); + if (!empty($versuche)) { + netdiagPdfEnsureSpace($pdf, 10); + $pdf->SetFont('', 'B', 8); + $pdf->Cell(180, 5, 'Versuche je Transportweg', 1, 1, 'L'); + $pdf->SetFont('', '', 8); + foreach ($versuche as $v) { + if (!is_array($v)) { + continue; + } + $zeile = (string) ($v['transport'] ?? '?'); + if (!empty($v['antwort'])) { + $zeile .= ': '.$v['antwort']; + } elseif (!empty($v['hinweis'])) { + $zeile .= ': '.$v['hinweis']; + } else { + $zeile .= ': keine Antwort'; + } + if (isset($v['laufzeitMs']) && $v['laufzeitMs'] !== null && $v['laufzeitMs'] !== '') { + $zeile .= ' ('.$v['laufzeitMs'].' ms)'; + } + netdiagPdfEnsureSpace($pdf, 5, function ($p) { + $p->SetFont('', 'B', 8); + $p->Cell(180, 5, 'Versuche je Transportweg — Fortsetzung', 1, 1, 'L'); + $p->SetFont('', '', 8); + }); + $pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext($zeile)), 1, 'L'); + } + } + + // Gegenstelle und beherrschte Methoden — Beleg dafür, WAS dort antwortet + $rest = array(); + if (!empty($data['gegenstelle'])) { + $rest[] = 'Gegenstelle: '.$data['gegenstelle']; + } + if (!empty($data['unterstuetzteMethoden'])) { + $rest[] = 'Unterstützte Methoden: '.$data['unterstuetzteMethoden']; + } + if (!empty($data['zielHerkunft'])) { + $rest[] = 'Ziel: '.$data['zielHerkunft']; + } + if (!empty($rest)) { + netdiagPdfEnsureSpace($pdf, 8); + $pdf->MultiCell(180, 5, netdiagPdfText(netdiagKundentext(implode(' | ', $rest))), 1, 'L'); + } + + // Grenze der Aussage zum Schluss — im Kundendokument unverzichtbar: + // gemessen ist die Erreichbarkeit, NICHT die Sprachqualität. + foreach (array('hinweis', 'verfahren') as $feld) { + if (!empty($data[$feld])) { + netdiagPdfEnsureSpace($pdf, 8); + $pdf->SetFont('', 'I', 7); + $pdf->MultiCell(180, 4, netdiagPdfText(netdiagKundentext((string) $data[$feld])), 1, 'L'); + $pdf->SetFont('', '', 8); + } + } +}