Vermittlungsserver: coturn mit Zugangsdaten, die verfallen
Damit Anrufe auch in Netzen zustande kommen, die keine direkte Verbindung zulassen (15-25 % der Faelle). coturn ist installiert, steht aber still, bis die Konfiguration liegt -- ein coturn mit Werkseinstellung ist ein offenes Relais. KEIN FESTES PASSWORT. Es laege dauerhaft im Browser jedes Team-Mitglieds und liesse sich nie entziehen. Der Server rechnet stattdessen bei jeder Abfrage Zugangsdaten, die nach zwoelf Stunden verfallen (server/workspace-turn.js, coturns `use-auth-secret`). Das Geheimnis liegt in einer Datei, nicht in der Datenbank: einstellung- Setzen() schreibt Werte ins Protokoll, und coturn braucht denselben Wert ohnehin in /etc. Die Oberflaeche frischt die Daten vor jedem Anruf auf. Der Chat ist eine App, die tagelang offen bleibt -- wer nur beim Laden holt, telefoniert am zweiten Tag ohne Vermittlung, und es faellt nicht auf: Es scheitern nur die, die sie gebraucht haetten. DIE WICHTIGSTE ZEILE DER KONFIGURATION ist die Sperrliste. Gemessen: dreizehn Dienste lauschen auf diesem Server nur oertlich, darunter Caddys Verwaltung auf 127.0.0.1:2019 -- wer sie erreicht, kann jede Website umleiten. Ohne Sperrliste waere der Vermittlungsserver die Tuer dorthin, und die Anfrage saehe fuer Caddy aus wie von localhost. Geprueft: pruef-anruf.mjs 51 -> 73 Pruefungen. Gegenprobe (Geheimnis als Passwort ausliefern + fremde Zugangsdaten ueberschreiben) macht genau 5 rot, darunter "das Geheimnis steht NIRGENDS in der Antwort". tools/turn-probelauf.sh beweist am echten coturn, was ein fester Vergleichswert nicht kann: dass coturn unsere Rechnung akzeptiert. Beide Seiten koennten sonst konsequent falsch rechnen und jede Pruefung waere gruen. Seine eigene Gegenprobe war zweimal zu Recht rot -- der erste Aufbau mass wegen `-y` gar nicht das Ziel, das er zu messen behauptete, sondern coturns eingebauten Loopback-Schutz. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+161
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
Aufrufen mit: node server/pruef-anruf.mjs
|
||||
===================================================================== */
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { mkdtempSync, rmSync, writeFileSync, unlinkSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { request as httpAnfrage } from "node:http";
|
||||
@@ -28,6 +28,11 @@ process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
process.env.PORT = String(PORT);
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||||
/* Der Vermittlungsserver holt sein Geheimnis aus einer Datei. Hier muss
|
||||
der Pfad UMGEBOGEN werden, bevor index.js geladen wird -- sonst
|
||||
zeigte er auf die echte Datei unter /etc, und die Pruefung wuerde
|
||||
entweder am echten Betrieb messen oder gar nichts sehen koennen. */
|
||||
process.env.TURN_GEHEIMNIS_DATEI = join(ordner, "turn.geheimnis");
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const ec = express.response.cookie;
|
||||
@@ -268,6 +273,161 @@ melde("=== 6. Die Verbindungsadressen ===");
|
||||
"danach ist die Liste wieder leer -- der naechste Abschnitt faengt sauber an");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
6b. DIE ZUGANGSDATEN ZUM VERMITTLUNGSSERVER
|
||||
|
||||
Hier steht die Frage, an der sich entscheidet, ob coturn ein Dienst
|
||||
fuer uns ist oder eine offene Weiterleitung fuer jeden: Das
|
||||
Geheimnis, aus dem die Zugangsdaten gerechnet werden, darf den
|
||||
Browser NIE erreichen. Wer es haette, koennte sich selbst beliebig
|
||||
viele Zugaenge ausstellen -- dauerhaft, ohne Konto, ueber unsere
|
||||
IP-Adresse.
|
||||
|
||||
Deshalb wird hier nicht nur geprueft, dass Zugangsdaten ANKOMMEN,
|
||||
sondern vor allem, dass das Geheimnis es NICHT tut -- und zwar im
|
||||
rohen Antworttext, nicht im ausgewerteten Objekt. Ein Feld, an das
|
||||
niemand denkt, faellt beim Auswerten nicht auf.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 6b. Zugangsdaten fuer die Vermittlung ===");
|
||||
{
|
||||
const turn = await import("./workspace-turn.js");
|
||||
const datei = process.env.TURN_GEHEIMNIS_DATEI;
|
||||
/* Erfunden, nur fuer diesen Lauf, und am Ende wieder weg. Lang genug,
|
||||
dass die Laengenpruefung nicht zufaellig anschlaegt. */
|
||||
const GEHEIM = "pruef-geheimnis-nur-fuer-diesen-lauf-0123456789";
|
||||
|
||||
/* --- Die drei Ausgaenge beim Lesen ------------------------------- */
|
||||
{
|
||||
const g = turn.turnGeheimnisLesen(datei);
|
||||
ok(g.wert === "" && g.grund === "fehlt",
|
||||
`ohne Datei sagt es "fehlt" statt stillschweigend nichts (${g.grund})`);
|
||||
}
|
||||
{
|
||||
writeFileSync(datei, "zu-kurz");
|
||||
const g = turn.turnGeheimnisLesen(datei);
|
||||
ok(g.wert === "" && g.grund === "zu_kurz",
|
||||
`ein zu kurzes Geheimnis wird abgelehnt, nicht benutzt (${g.grund})`);
|
||||
}
|
||||
{
|
||||
writeFileSync(datei, " \n");
|
||||
const g = turn.turnGeheimnisLesen(datei);
|
||||
ok(g.wert === "" && g.grund === "leer", `eine leere Datei meldet "leer" (${g.grund})`);
|
||||
}
|
||||
{
|
||||
writeFileSync(datei, GEHEIM + "\n");
|
||||
const g = turn.turnGeheimnisLesen(datei);
|
||||
ok(g.wert === GEHEIM && g.grund === "",
|
||||
"mit Datei kommt das Geheimnis an (Zeilenumbruch abgeschnitten)");
|
||||
}
|
||||
|
||||
/* --- Ein WECHSEL muss auffallen ---------------------------------- */
|
||||
{
|
||||
writeFileSync(datei, GEHEIM + "-zweite-fassung-mit-anderer-laenge");
|
||||
const g = turn.turnGeheimnisLesen(datei);
|
||||
ok(g.wert.endsWith("-zweite-fassung-mit-anderer-laenge"),
|
||||
"wird die Datei getauscht, wird der neue Wert gelesen (kein Festhaengen)");
|
||||
writeFileSync(datei, GEHEIM + "\n");
|
||||
}
|
||||
|
||||
/* --- Die Rechnung ------------------------------------------------ */
|
||||
{
|
||||
/* Eine FESTE Zeit als Eingabe, nicht die Wanduhr: Sonst waere die
|
||||
Pruefung eine Zeitbombe, die irgendwann etwas anderes misst. */
|
||||
const JETZT = 1_700_000_000_000;
|
||||
const z = turn.turnZugang("w7", GEHEIM, JETZT);
|
||||
const [ablaufStr, kennung] = z.username.split(":");
|
||||
ok(kennung === "w7", `im Benutzernamen steht die Kennung (${kennung})`);
|
||||
ok(Number(ablaufStr) === Math.floor(JETZT / 1000) + turn.TURN_GUELTIG_STUNDEN * 3600,
|
||||
`und davor der Ablauf, genau ${turn.TURN_GUELTIG_STUNDEN} Stunden spaeter`);
|
||||
|
||||
/* FESTER VERGLEICHSWERT, ausgerechnet und nicht behauptet. Er
|
||||
beweist NICHT, dass coturn ihn akzeptiert -- das beweist nur ein
|
||||
Versuch am laufenden coturn (turnutils_uclient). Er beweist,
|
||||
dass sich die Rechenvorschrift nicht unbemerkt aendert; genau
|
||||
das wuerde jeden Anruf ueber die Vermittlung scheitern lassen,
|
||||
ohne dass irgendwo ein Fehler steht. */
|
||||
ok(z.credential === "9AbMBJeTDL6uebcR6KRpYVTFwt8=",
|
||||
`das Passwort ist die bekannte HMAC-SHA1-Rechnung (${z.credential})`);
|
||||
|
||||
const nochmal = turn.turnZugang("w7", GEHEIM, JETZT);
|
||||
ok(nochmal.credential === z.credential, "dieselbe Eingabe ergibt dasselbe Passwort");
|
||||
const anderer = turn.turnZugang("w8", GEHEIM, JETZT);
|
||||
ok(anderer.credential !== z.credential,
|
||||
"eine andere Person bekommt ein anderes Passwort");
|
||||
const spaeter = turn.turnZugang("w7", GEHEIM, JETZT + 60_000);
|
||||
ok(spaeter.credential !== z.credential,
|
||||
"und eine Minute spaeter ein anderes -- die Daten verfallen wirklich");
|
||||
}
|
||||
|
||||
/* --- Wer bekommt Zugangsdaten und wer nicht ---------------------- */
|
||||
{
|
||||
const liste = [
|
||||
{ urls: "stun:turn.dogfather-universe.com:3478" },
|
||||
{ urls: "turn:turn.dogfather-universe.com:3478" },
|
||||
{ urls: "turn:fremder-dienst.example:3478", username: "fremd", credential: "eigenes" },
|
||||
];
|
||||
const e = turn.mitZugangsdaten(liste, "w7", 1_700_000_000_000, datei);
|
||||
ok(!e.adressen[0].credential, "ein STUN-Eintrag bekommt keine Zugangsdaten");
|
||||
ok(!!e.adressen[1].credential && e.adressen[1].username.includes(":w7"),
|
||||
"unser TURN-Eintrag bekommt gerechnete");
|
||||
ok(e.adressen[2].credential === "eigenes" && e.adressen[2].username === "fremd",
|
||||
"ein fremder Dienst behaelt seine eigenen -- wir ueberschreiben nichts");
|
||||
ok(e.turn_gesamt === 2 && e.turn_bereit === 2,
|
||||
`und die Zahlen stimmen (${e.turn_bereit} von ${e.turn_gesamt} bereit)`);
|
||||
}
|
||||
{
|
||||
/* OHNE Geheimnis: Der Eintrag bleibt stehen, aber es wird gesagt. */
|
||||
unlinkSync(datei);
|
||||
const e = turn.mitZugangsdaten(
|
||||
[{ urls: "turn:turn.dogfather-universe.com:3478" }], "w7", Date.now(), datei);
|
||||
ok(e.turn_gesamt === 1 && e.turn_bereit === 0 && e.grund === "fehlt",
|
||||
`ohne Geheimnis ist nichts bereit, und der Grund steht dabei (${e.grund})`);
|
||||
writeFileSync(datei, GEHEIM + "\n");
|
||||
}
|
||||
|
||||
/* --- Und jetzt ueber die Schnittstelle, wie der Browser sie sieht - */
|
||||
{
|
||||
await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }, {
|
||||
adressen: [{ urls: "turn:turn.dogfather-universe.com:3478" }],
|
||||
}, "PUT");
|
||||
const a = await roh("/workspace/api/anruf/adressen", CREW, { cookie: kModi });
|
||||
const r = json(a);
|
||||
|
||||
ok(r.turn_gesamt === 1 && r.turn_bereit === 1,
|
||||
`der Modi bekommt einen einsatzbereiten Eintrag (${r.turn_bereit}/${r.turn_gesamt})`);
|
||||
ok(!!r.adressen?.[0]?.credential && !!r.adressen?.[0]?.username,
|
||||
"mit Benutzername und Passwort");
|
||||
|
||||
/* DIE WICHTIGSTE PRUEFUNG DES ABSCHNITTS. Im ROHEN Text gesucht,
|
||||
nicht im ausgewerteten Objekt. */
|
||||
ok(!a.text.includes(GEHEIM),
|
||||
"und das Geheimnis selbst steht NIRGENDS in der Antwort");
|
||||
|
||||
/* Jeder bekommt EIGENE Zugangsdaten -- im coturn-Protokoll steht
|
||||
damit die Nummer, nicht ein Sammelkonto. */
|
||||
const rDogi = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
|
||||
ok(rDogi.adressen?.[0]?.username !== r.adressen?.[0]?.username,
|
||||
"DogFather und der Modi bekommen verschiedene Zugangsdaten");
|
||||
|
||||
/* Der Ablauf muss fuer den Browser LESBAR sein -- darauf beruht das
|
||||
Auffrischen vor jedem Anruf in anruf.js. */
|
||||
const ablauf = Number(String(r.adressen?.[0]?.username || "").split(":")[0]);
|
||||
ok(ablauf > Math.floor(Date.now() / 1000) + 3600,
|
||||
`der Ablauf steht vorn im Benutzernamen und liegt in der Zukunft (${ablauf})`);
|
||||
}
|
||||
|
||||
/* --- Aufraeumen: der naechste Abschnitt faengt sauber an ---------- */
|
||||
{
|
||||
await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi },
|
||||
{ adressen: [] }, "PUT");
|
||||
try { unlinkSync(datei); } catch { /* schon weg */ }
|
||||
const zurueck = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
|
||||
ok(zurueck.adressen?.length === 0 && !zurueck.turn_grund,
|
||||
"danach ist wieder alles leer -- keine erfundene Adresse bleibt stehen");
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
7. IM BROWSER
|
||||
======================================================================= */
|
||||
|
||||
Reference in New Issue
Block a user