Ein turn: ohne Zugangsdaten haette JEDEN Anruf verhindert, nicht nur die vermittelten

Gefunden am 25.09.2026 beim Bau des Podcast-Studios, das dieselbe
Rechnung benutzt, und dort im echten Browser gemessen:

  Failed to construct 'RTCPeerConnection': Both username and credential
  are required when the URL scheme is "turn" or "turns".

`new RTCPeerConnection(...)` WIRFT. Die Folge ist nicht "kein
Vermittlungsserver", sondern KEIN ANRUF -- auch nicht der, der direkt
zustande gekommen waere. Aus einem Randproblem (etwa jeder Fuenfte
braucht Vermittlung) waere ein Totalausfall fuer alle geworden,
ausgeloest davon, dass /etc/coturn-workspace.geheimnis einmal nicht
lesbar ist: Rechte verstellt, beim Neuaufbau vergessen, Datei leer.

server/workspace-turn.js laesst solche Eintraege bewusst stehen, damit
der Grund nicht verschwindet ("KEIN STILLES WEGLASSEN"). Das ist dort
richtig -- es heisst aber, dass im Browser aussortiert werden muss,
bevor die Verbindung gebaut wird. Genau das fehlte. Der Kommentar
daneben behauptete "ein turn: ohne Passwort schadet nicht"; das war
nie gemessen und ist falsch.

Zwei Aenderungen in workspace/assets/js/anruf.js:

1. Beim Uebernehmen der Adressen werden turn:/turns:-Eintraege ohne
   Benutzer UND Passwort aussortiert. STUN bleibt -- es kennt gar keine
   Anmeldung und ist der Weg, der in den meisten Faellen reicht. Der
   Grund geht nicht verloren, er steht weiter in der Konsole (jetzt mit
   dem Dateinamen, in dem man nachsehen muss).

2. `adressenFrisch()` sagt bei gesetztem Grund immer "nicht frisch".
   Sonst waere die Liste fuer immer frisch: Ohne Zugangsdaten bleibt nur
   STUN uebrig, und ein STUN-Eintrag hat keinen Ablaufzeitpunkt --
   `every()` sagt dann "alles gueltig" und es wird nie wieder gefragt.
   Waere die Datei auf dem Server repariert, telefonierte trotzdem
   niemand mehr mit Vermittlung, bis er die Seite neu laedt. (Nebenbei
   die zweite Falle derselben Zeile: `every()` auf einer LEEREN Liste
   ist `true`.)

Geprueft in server/pruef-anruf.mjs: Die Pruefung liest den Filter aus
der ausgelieferten Datei und FUEHRT IHN AUS -- vier Adressen hinein,
zwei brauchbare heraus, mit Gegenprobe, dass er ueberhaupt etwas
wegnimmt. Eine Pruefung, die nur nachsieht, ob irgendwo "filter" steht,
waere auch dann gruen, wenn der Filter das Falsche tut.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-25 17:02:23 +02:00
co-authored by Claude Opus 5
parent cdb6c46f8b
commit 29d339b262
2 changed files with 106 additions and 4 deletions
+54 -1
View File
@@ -16,7 +16,7 @@
Aufrufen mit: node server/pruef-anruf.mjs
===================================================================== */
import { mkdtempSync, rmSync, writeFileSync, unlinkSync } from "node:fs";
import { mkdtempSync, rmSync, writeFileSync, unlinkSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
@@ -1224,6 +1224,59 @@ melde("=== 8. Zu dritt ===");
}
}
/* =====================================================================
EIN turn: OHNE ZUGANGSDATEN DARF DEN BROWSER NICHT ERREICHEN
(25.09.2026)
Am selben Tag beim Bau des Podcast-Studios im echten Browser
nachgemessen:
Failed to construct 'RTCPeerConnection': Both username and
credential are required when the URL scheme is "turn" or "turns".
`new RTCPeerConnection(...)` WIRFT. Die Folge ist nicht "kein
Vermittlungsserver", sondern KEIN ANRUF -- auch nicht der, der direkt
zustande gekommen waere. Aus einem Randproblem (etwa jeder Fuenfte
braucht Vermittlung) wuerde ein Totalausfall fuer alle, ausgeloest
davon, dass eine Datei auf dem Server unlesbar ist.
Der Server laesst solche Eintraege bewusst stehen, damit der Grund
nicht verschwindet -- das ist dort richtig. Aussortiert wird im
Browser, in `workspace/assets/js/anruf.js`.
DIESE PRUEFUNG LIEST KEINEN TEXT, SONDERN FUEHRT DEN ECHTEN AUSDRUCK
AUS. Eine Prüfung, die nur nachsieht, ob irgendwo das Wort "filter"
steht, waere auch dann gruen, wenn der Filter das Falsche tut.
===================================================================== */
{
const quelle = readFileSync(new URL("../workspace/assets/js/anruf.js", import.meta.url), "utf8");
const t = /adressen = \(d\.adressen \|\| \[\]\)(\.filter\([\s\S]*?\n {8}\}\);)/.exec(quelle);
ok(!!t, "in anruf.js wird die Adressliste ueberhaupt gefiltert");
if (t) {
/* Genau der Ausdruck aus der ausgelieferten Datei, ausgefuehrt. */
const filtern = new Function("d", "return (d.adressen || [])" + t[1] + ";");
const rein = {
adressen: [
{ urls: "stun:1.2.3.4:3478" },
{ urls: "turn:1.2.3.4:3478?transport=udp", username: "1:a", credential: "x" },
{ urls: "turn:1.2.3.4:3478?transport=tcp" },
{ urls: "turns:1.2.3.4:5349", username: "1:a" },
],
};
const raus = filtern(rein);
ok(raus.length === 2, `es bleiben genau die zwei brauchbaren uebrig (${raus.length})`);
ok(raus.some((a) => a.urls.startsWith("stun:")),
"STUN bleibt drin -- es kennt gar keine Anmeldung und reicht meistens");
ok(raus.every((a) => !/^turns?:/.test(a.urls) || (a.username && a.credential)),
"kein turn: ohne Benutzer UND Passwort geht hinaus");
/* GEGENPROBE: Wuerde der Filter alles durchlassen, waere die Zeile
oben auch gruen. Hier wird bewiesen, dass er wirklich etwas
wegnimmt. */
ok(raus.length < rein.adressen.length,
"Gegenprobe: der Filter nimmt tatsaechlich etwas weg");
}
}
/* ------------------------------------------------------------------- */
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`