netdiag-app/tools/pruefe-sip.mjs
Eduard Wisch 3f29a373ac
All checks were successful
Build APK / build-apk (push) Successful in 2m48s
SIP-Erreichbarkeit, Fingerabdruck-Anmeldung, Token raus aus der Adresszeile [apk]
Die vier letzten offenen Punkte der Roadmap.

SIP-Erreichbarkeit (neues Werkzeug, Kategorie Telefonie)
- Sip.kt: OPTIONS nach RFC 3261 ueber UDP, bei Schweigen TCP nachgeschoben.
  Antwortet nur TCP, ist das der Fingerabdruck einer Firewall, die UDP
  verwirft - eine andere Reparatur als "Anlage aus".
- sip-bewertung.ts ohne Laufzeit-Import, damit tools/pruefe-sip.mjs die Regeln
  direkt nachrechnen kann (14 Faelle + 4 Texte, alle korrekt).
- Leitsatz: JEDE Statuszeile beweist eine antwortende Instanz. 401/407 sind
  deshalb gruen mit Hinweis "Anmeldung verlangt" - beim Provider der
  Normalzustand. Als Ausfall gemeldet wuerde der Techniker am falschen Ende
  suchen.
- RTP-Sprachqualitaet bewusst NICHT: ohne aufgebautes Gespraech nicht ehrlich
  messbar. Die Grenze steht im Ergebnis und landet im Kunden-PDF.

Fingerabdruck-Anmeldung (Biometrie.kt, BiometricPrompt + Android-Keystore)
- Der Kern ist nicht der Dialog, sondern was danach passiert: die
  Klartextfassung des Tokens wird aus den Capacitor Preferences ENTFERNT.
  Vorher stand es offen in CapacitorStorage.xml und war auf einem entsperrten
  Geraet ohne Weiteres lesbar - ein zusaetzlich verschluesseltes Token haette
  daran nichts geaendert.
- Kein zweiter Faktor: der Finger ersetzt das Passwort (Eddys Vorgabe).
  Gueltigkeit und Widerruf bleiben bei awlauth - hinterlegt ist dasselbe Token.
- 401 unterscheidet jetzt "Token abgelehnt" von "gar nicht angemeldet". Ohne
  das loeschte der erste Startaufruf die gerade eingerichtete Anmeldung wieder
  (im Emulator aufgefallen, Ursache im logcat).

Token nicht mehr in der Adresszeile
- updateDownloadUrl() ohne ?jwt=; der APK-Herunterlader im Plugin schickt
  Authorization: Bearer. instanceFollowRedirects=false, sonst ginge der Header
  bei einer Weiterleitung an ein fremdes Ziel mit.
- pdfUrl() war seit dem Initialcommit tot (kein Aufrufer) und ist durch
  fetchProtocolPdf() ersetzt - Header statt Token in der URL.

Im Emulator geprueft: SIP gegen einen eigenen Responder (200/401/Port zu),
Fingerabdruck vollstaendig durchgespielt (einrichten, Neustart, anmelden,
abbrechen), Speicher jeweils nachgesehen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:40:27 +02:00

144 lines
4.7 KiB
JavaScript

/**
* Prueft die Bewertung der SIP-Erreichbarkeit gegen konstruierte Antworten.
*
* Der Kernsatz, der hier nachgerechnet wird: JEDE Statuszeile beweist eine
* antwortende Instanz. Eine 401 ("bitte anmelden") oder eine 405 ("Methode
* nicht erlaubt") darf deshalb NIE als Ausfall im Kundendokument landen -
* genau das waere die Falschaussage, wegen der ein Techniker am falschen Ende
* sucht. Ebenso wichtig: der Fall "nur ueber TCP kam etwas" muss auffallen,
* weil er auf einen UDP-Filter zeigt, waehrend die Anlage selbst laeuft.
*
* Start: node tools/pruefe-sip.mjs
*/
const { bewerteSip, fehlerText } = await import('../src/lib/tools/telefonie/sip-bewertung.ts');
const antwort = (transport, code, text, extra = {}) => ({
transport,
statusCode: code,
statusText: text,
latenzMs: 12,
server: extra.server ?? null,
allow: extra.allow ?? null,
fehler: null,
});
const stumm = (transport, grund = 'timeout') => ({
transport,
statusCode: null,
statusText: null,
latenzMs: null,
server: null,
allow: null,
fehler: grund,
});
const m = (versuche, host = 'fritz.box', port = 5060) => ({ host, ip: '192.168.1.1', port, versuche });
const faelle = [
{
name: '200 OK ueber UDP -> GRUEN',
m: m([antwort('udp', 200, 'OK', { server: 'FRITZ!OS' })]),
soll: 0,
},
{
name: '401 Unauthorized -> GRUEN + Anmeldung verlangt (Normalfall beim Provider)',
m: m([antwort('udp', 401, 'Unauthorized')], 'sip.stadtwerke-nms.de'),
soll: 0,
pruefe: (r) => r.anmeldungNoetig === true,
},
{
name: '407 Proxy Authentication Required -> GRUEN + Anmeldung verlangt',
m: m([antwort('udp', 407, 'Proxy Authentication Required')]),
soll: 0,
pruefe: (r) => r.anmeldungNoetig === true,
},
{
name: '405 Method Not Allowed -> GELB, aber ERREICHBAR (kein Ausfall!)',
m: m([antwort('udp', 405, 'Method Not Allowed')]),
soll: 1,
pruefe: (r) => r.statuszeile === '405 Method Not Allowed',
},
{
name: '404 Not Found -> GELB (Instanz da, Ziel unbekannt)',
m: m([antwort('udp', 404, 'Not Found')]),
soll: 1,
},
{
name: '403 Forbidden -> GELB (erreichbar, weist ab)',
m: m([antwort('udp', 403, 'Forbidden')]),
soll: 1,
},
{
name: '503 Service Unavailable -> GELB (Weg in Ordnung, Gegenseite gestoert)',
m: m([antwort('udp', 503, 'Service Unavailable')]),
soll: 1,
},
{
name: 'UDP stumm, TCP antwortet 200 -> GELB + Hinweis auf UDP-Filter',
m: m([stumm('udp'), antwort('tcp', 200, 'OK')]),
soll: 1,
pruefe: (r) => r.antwortWeg === 'tcp' && /UDP/.test(r.befund),
},
{
name: 'UDP stumm, TCP antwortet 401 -> GELB (nicht gruen: der Filter bleibt ein Befund)',
m: m([stumm('udp'), antwort('tcp', 401, 'Unauthorized')]),
soll: 1,
pruefe: (r) => r.anmeldungNoetig === true,
},
{
name: 'Beide Wege stumm -> ROT',
m: m([stumm('udp'), stumm('tcp')]),
soll: 2,
pruefe: (r) => r.statuszeile === null,
},
{
name: 'Verbindung abgewiesen (Port zu) -> ROT, aber NICHT als "keine Antwort" betitelt',
m: m([stumm('udp'), stumm('tcp', 'abgewiesen')]),
soll: 2,
pruefe: (r) => /Port geschlossen/.test(r.kurz) && /erreichbar/.test(r.befund),
},
{
name: 'Name nicht aufloesbar -> NICHT MESSBAR (DNS-Befund, keine Aussage ueber die Anlage)',
m: m([stumm('dns', 'name')], 'pbx.kunde.local'),
soll: 3,
},
{
name: 'Antwort ist kein SIP -> ROT mit eigenem Befund (nicht "keine Antwort")',
m: m([stumm('udp', 'kein-sip')]),
soll: 2,
pruefe: (r) => /kein SIP/.test(r.kurz),
},
{
name: 'Nicht-Standardport erscheint im Kurztext',
m: m([antwort('udp', 200, 'OK')], 'pbx.kunde.de', 5080),
soll: 0,
pruefe: (r) => r.kurz.includes('pbx.kunde.de:5080'),
},
];
let fehler = 0;
for (const f of faelle) {
const r = bewerteSip(f.m);
const statusOk = r.status === f.soll;
const extraOk = f.pruefe ? f.pruefe(r) : true;
if (!statusOk || !extraOk) fehler++;
console.log(`${statusOk && extraOk ? 'OK ' : 'FEHL'} ${f.name}`);
console.log(` status=${r.status} (soll ${f.soll}) ${r.kurz}`);
if (!extraOk) console.log(` Zusatzpruefung fehlgeschlagen: ${r.befund}`);
}
// Die Fehlertexte muessen den Port nennen - "keine Antwort" allein schickt den
// Techniker sonst auf die Suche ohne Anhaltspunkt.
const textFaelle = [
['timeout', 5060, /Port 5060/],
['abgewiesen', 5080, /Port 5080/],
['kein-sip', 5060, /kein SIP/],
['name', 5060, /aufl/],
];
for (const [grund, port, muster] of textFaelle) {
const t = fehlerText(grund, port);
const okk = muster.test(t);
if (!okk) fehler++;
console.log(`${okk ? 'OK ' : 'FEHL'} fehlerText("${grund}", ${port}) = "${t}"`);
}
console.log(fehler === 0 ? 'ALLE FAELLE KORREKT' : `${fehler} FEHLER`);
process.exit(fehler === 0 ? 0 : 1);