1.3.0: PDF-Zweig SIP-Erreichbarkeit, Token in der URL nur noch fuer den APK-Download [deploy]
All checks were successful
Deploy netdiag / deploy (push) Successful in 14s

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) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-08-19 18:29:51 +02:00
parent a0dc0750cb
commit 087f37afa6
7 changed files with 230 additions and 12 deletions

View file

@ -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

View file

@ -198,11 +198,24 @@ function netdiag_api_error($message, $httpstatus = 400)
/**
* Token aus Request lesen (Authorization-Header oder ?jwt=).
* Token aus Request lesen.
*
* 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 '';
@ -234,11 +247,12 @@ function netdiag_api_read_token()
* sobald alle Geräte einmal neu angemeldet sind (siehe ROADMAP_UMSETZUNG L5).
*
* @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);
}

View file

@ -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';

View file

@ -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';

View file

@ -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);

View file

@ -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',

View file

@ -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);
}
}
}