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
|
||||
======================================================================= */
|
||||
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
istDogFather,
|
||||
} from "./workspace.js";
|
||||
import { chatEreignis, istDrinFuerAnruf, teilnehmerFuerAnruf } from "./workspace-chat.js";
|
||||
import { mitZugangsdaten } from "./workspace-turn.js";
|
||||
|
||||
export const anrufRouter = express.Router();
|
||||
|
||||
@@ -161,13 +162,38 @@ anrufRouter.get("/workspace/api/anruf/adressen", (req, res) => {
|
||||
try {
|
||||
let liste = [];
|
||||
try { liste = JSON.parse(einstellung("anruf_adressen") || "[]"); } catch { liste = []; }
|
||||
|
||||
/* ZUGANGSDATEN WERDEN GERECHNET, NICHT GESPEICHERT.
|
||||
Warum das so ist, steht ausfuehrlich in workspace-turn.js. Kurz:
|
||||
Ein festes TURN-Passwort laege dauerhaft im Browser jedes
|
||||
Teilnehmers und liesse sich nie wieder entziehen. Die hier
|
||||
erzeugten verfallen von selbst.
|
||||
|
||||
Als Kennung die PERSONENNUMMER, nicht der Name -- sie landet im
|
||||
Protokoll von coturn, und das ist kein Ort fuer Klarnamen. */
|
||||
const { adressen, turn_gesamt, turn_bereit, grund } =
|
||||
mitZugangsdaten(liste, `w${req.person.id}`);
|
||||
|
||||
if (grund) {
|
||||
/* DER DRITTE AUSGANG. „Kann nicht nachsehen" ist etwas anderes
|
||||
als „ist nicht eingerichtet" -- und wer das verschweigt, sucht
|
||||
den Fehler spaeter beim Anrufer. */
|
||||
console.warn(`[anruf] TURN-Geheimnis nicht verwendbar: ${grund}`);
|
||||
}
|
||||
|
||||
res.json({
|
||||
adressen: Array.isArray(liste) ? liste : [],
|
||||
adressen,
|
||||
gruppe_max: GRUPPE_MAX,
|
||||
/* WAS DIE OBERFLAECHE WISSEN MUSS, um ehrlich zu sein: Ohne
|
||||
Adressen klappt der Anruf nur im selben Netz. Das gehoert
|
||||
DAVOR gesagt, nicht als Fehlermeldung danach. */
|
||||
nur_gleiches_netz: !Array.isArray(liste) || liste.length === 0,
|
||||
/* Zahlen statt Ja/Nein: Wer zaehlen kann, kann pruefen. Ein
|
||||
eingetragener TURN-Server, der mangels Geheimnis niemanden
|
||||
durchlaesst, sieht sonst genauso aus wie ein funktionierender. */
|
||||
turn_gesamt,
|
||||
turn_bereit,
|
||||
...(grund ? { turn_grund: grund } : {}),
|
||||
});
|
||||
} catch (f) {
|
||||
console.error("[anruf] Adressen:", f?.message);
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/* =====================================================================
|
||||
ZUGANGSDATEN FÜR DEN VERMITTLUNGSSERVER (TURN) -- 18.09.2026
|
||||
|
||||
Ein TURN-Server leitet Ton und Bild weiter, wenn zwei Browser sich
|
||||
nicht direkt erreichen (strenge Firmennetze, manche Mobilfunknetze).
|
||||
Er will wissen, wer ihn benutzen darf -- also Benutzer und Passwort.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM HIER GERECHNET WIRD, STATT EIN PASSWORT EINZUTRAGEN
|
||||
|
||||
Der naheliegende Weg wäre ein festes Passwort in den Einstellungen.
|
||||
Es wäre in zwei Minuten fertig -- und es läge danach dauerhaft im
|
||||
Browser jedes Team-Mitglieds, sichtbar in jedem Netzwerk-Fenster,
|
||||
gültig ohne Ende. Wer einmal dabei war, könnte den Server für immer
|
||||
als Weiterleitung benutzen: fremden Verkehr über unsere IP-Adresse
|
||||
schicken, unsere Bandbreite verbrauchen. Ein Zugang, den man nicht
|
||||
entziehen kann, ist kein Zugang, sondern ein Loch.
|
||||
|
||||
Deshalb der Weg, den coturn selbst dafür vorsieht
|
||||
(`use-auth-secret`): Der Server hier kennt ein GEHEIMNIS, das den
|
||||
Browser nie erreicht. Aus ihm rechnet er bei jeder Abfrage frische
|
||||
Zugangsdaten, die nach TURN_GUELTIG_STUNDEN von selbst verfallen.
|
||||
coturn rechnet dieselbe Zahl nach und kennt dafür nur das Geheimnis
|
||||
-- es muss keine Benutzerliste geben, und nichts muss abgeglichen
|
||||
werden.
|
||||
|
||||
Benutzername = <Ablaufzeitpunkt>:<Kennung>
|
||||
Passwort = base64( HMAC-SHA1( Geheimnis, Benutzername ) )
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM DAS GEHEIMNIS IN EINER DATEI LIEGT UND NICHT IN DER DATENBANK
|
||||
|
||||
Zwei Gründe, beide im Haus schon einmal teuer gewesen:
|
||||
|
||||
1. `einstellungSetzen()` schreibt den WERT in das Protokoll
|
||||
(`detail: "schluessel = wert"`). Ein Geheimnis dort wäre im
|
||||
Klartext in der Prüfspur -- und in jeder nächtlichen Sicherung.
|
||||
|
||||
2. coturn braucht denselben Wert in `/etc/turnserver.conf`. Eine
|
||||
Datei, die beide Seiten aus derselben Quelle bekommen, kann nicht
|
||||
auseinanderlaufen; zwei Eingabefelder können es immer.
|
||||
|
||||
Die Datei gehört root und ist für die Gruppe des Dienstes lesbar
|
||||
(640). Der Browser bekommt sie nie zu sehen -- das ist der Punkt,
|
||||
an dem `pruef-anruf.mjs` nachmisst.
|
||||
===================================================================== */
|
||||
|
||||
import { createHmac } from "node:crypto";
|
||||
import { readFileSync, statSync } from "node:fs";
|
||||
|
||||
/** Wie lange erzeugte Zugangsdaten gelten.
|
||||
*
|
||||
* Zwölf Stunden ist die Spanne, die einen Arbeitstag überdeckt, ohne
|
||||
* dass jemand mittendrin herausfliegt -- und kurz genug, dass ein
|
||||
* Zugang, der nicht mehr gelten soll, am nächsten Tag nicht mehr
|
||||
* gilt. Die Oberfläche holt die Adressen zusätzlich bei jedem Anruf
|
||||
* neu; deshalb ist diese Zahl eine Obergrenze, kein Takt. */
|
||||
export const TURN_GUELTIG_STUNDEN = 12;
|
||||
|
||||
/** Wo das gemeinsame Geheimnis liegt.
|
||||
*
|
||||
* Über eine Umgebungsvariable umstellbar -- nicht aus Vorliebe für
|
||||
* Einstellbarkeit, sondern weil die Prüfung sonst entweder gar nicht
|
||||
* messen könnte oder an die echte Datei müsste. */
|
||||
export const TURN_GEHEIMNIS_DATEI =
|
||||
process.env.TURN_GEHEIMNIS_DATEI || "/etc/coturn-workspace.geheimnis";
|
||||
|
||||
/* Gelesenes Geheimnis, gemerkt über einen ÄNDERUNGSSTEMPEL statt über
|
||||
eine Laufzeit: Wer die Datei austauscht, soll nicht bis zum Ablauf
|
||||
einer Frist warten. Ein `stat` je Abfrage kostet nichts, und die
|
||||
Abfrage kommt ein paar Mal am Tag, nicht ein paar Mal je Sekunde. */
|
||||
let gemerkt = { stempel: "", wert: "" };
|
||||
|
||||
/** Liest das Geheimnis.
|
||||
*
|
||||
* DREI AUSGÄNGE, nicht zwei -- „ich kann nicht nachsehen" ist etwas
|
||||
* anderes als „es ist keins da". Der Grund wandert bis in die Antwort
|
||||
* der Schnittstelle, damit an der Oberfläche nicht geraten wird.
|
||||
*
|
||||
* @returns {{wert: string, grund: string}} `grund` ist leer, wenn
|
||||
* alles in Ordnung ist; sonst `fehlt`, `leer`, `zu_kurz` oder
|
||||
* `unlesbar:<CODE>`.
|
||||
*/
|
||||
export function turnGeheimnisLesen(datei = TURN_GEHEIMNIS_DATEI) {
|
||||
let s;
|
||||
try {
|
||||
s = statSync(datei);
|
||||
} catch (f) {
|
||||
gemerkt = { stempel: "", wert: "" };
|
||||
return { wert: "", grund: f?.code === "ENOENT" ? "fehlt" : `unlesbar:${f?.code || "?"}` };
|
||||
}
|
||||
const stempel = `${s.mtimeMs}:${s.size}`;
|
||||
if (stempel !== gemerkt.stempel) {
|
||||
try {
|
||||
gemerkt = { stempel, wert: readFileSync(datei, "utf8").trim() };
|
||||
} catch (f) {
|
||||
gemerkt = { stempel: "", wert: "" };
|
||||
return { wert: "", grund: `unlesbar:${f?.code || "?"}` };
|
||||
}
|
||||
}
|
||||
if (!gemerkt.wert) return { wert: "", grund: "leer" };
|
||||
/* Ein zu kurzes Geheimnis ist schlimmer als keins: Es sieht aus wie
|
||||
Sicherheit und ist in Minuten geraten. 32 Zeichen entsprechen den
|
||||
16 Bytes, die der Einrichtungsbefehl erzeugt. */
|
||||
if (gemerkt.wert.length < 32) return { wert: "", grund: "zu_kurz" };
|
||||
return { wert: gemerkt.wert, grund: "" };
|
||||
}
|
||||
|
||||
/** Rechnet ein Paar Zugangsdaten aus.
|
||||
*
|
||||
* @param kennung Wer fragt -- landet im Benutzernamen und damit im
|
||||
* Protokoll von coturn. Keine Namen, nur die Nummer: Das Protokoll
|
||||
* eines Vermittlungsservers ist kein Ort für Klarnamen.
|
||||
* @param geheimnis Der gemeinsame Wert aus der Datei.
|
||||
* @param jetzt Millisekunden. Als Eingabe übergeben, nicht aus der
|
||||
* Wanduhr gelesen -- sonst wäre die Prüfung eine Zeitbombe.
|
||||
*/
|
||||
export function turnZugang(kennung, geheimnis, jetzt = Date.now()) {
|
||||
const ablauf = Math.floor(jetzt / 1000) + TURN_GUELTIG_STUNDEN * 3600;
|
||||
const username = `${ablauf}:${kennung}`;
|
||||
return {
|
||||
username,
|
||||
credential: createHmac("sha1", geheimnis).update(username).digest("base64"),
|
||||
ablauf,
|
||||
};
|
||||
}
|
||||
|
||||
/** Ergänzt eine Adressliste um frische Zugangsdaten.
|
||||
*
|
||||
* WELCHER EINTRAG BEKOMMT WELCHE BEHANDLUNG:
|
||||
*
|
||||
* stun:… nichts -- STUN kennt keine Anmeldung.
|
||||
* turn:… ohne eigenes Passwort gerechnete Zugangsdaten (unser
|
||||
* coturn).
|
||||
* turn:… MIT username+credential unverändert -- das ist ein
|
||||
* fremder Dienst, den jemand bewusst
|
||||
* eingetragen hat. Wir überschreiben
|
||||
* keine Zugangsdaten, die wir nicht
|
||||
* ausgestellt haben.
|
||||
*
|
||||
* @returns {{adressen: object[], turn_gesamt: number,
|
||||
* turn_bereit: number, grund: string}}
|
||||
* Die ZAHLEN gehören zurückgegeben, nicht nur die Liste: Eine
|
||||
* Prüfung, die „ist grün" sagen soll, muss zählen können, worüber
|
||||
* sie redet -- sonst ist sie auch bei einer leeren Liste grün.
|
||||
*/
|
||||
export function mitZugangsdaten(liste, kennung, jetzt = Date.now(),
|
||||
datei = TURN_GEHEIMNIS_DATEI) {
|
||||
const rein = Array.isArray(liste) ? liste : [];
|
||||
const brauchtZugang = (e) =>
|
||||
/^turns?:/.test(String(e?.urls || "")) && !e?.credential;
|
||||
const turn_gesamt = rein.filter((e) => /^turns?:/.test(String(e?.urls || ""))).length;
|
||||
|
||||
if (!rein.some(brauchtZugang)) {
|
||||
return { adressen: rein, turn_gesamt, turn_bereit: turn_gesamt, grund: "" };
|
||||
}
|
||||
|
||||
const { wert, grund } = turnGeheimnisLesen(datei);
|
||||
if (!wert) {
|
||||
/* KEIN STILLES WEGLASSEN. Ohne Zugangsdaten weist coturn jede
|
||||
Anfrage ab -- der Anruf fällt dann auf die direkte Verbindung
|
||||
zurück und scheitert genau bei den Leuten, für die der Server
|
||||
gebaut wurde. Der Eintrag bleibt trotzdem in der Liste: Ein
|
||||
`turn:` ohne Passwort schadet nicht, und der Grund steht dabei. */
|
||||
return {
|
||||
adressen: rein,
|
||||
turn_gesamt,
|
||||
turn_bereit: rein.filter((e) => /^turns?:/.test(String(e?.urls || "")) && e?.credential).length,
|
||||
grund,
|
||||
};
|
||||
}
|
||||
|
||||
const zugang = turnZugang(kennung, wert, jetzt);
|
||||
return {
|
||||
adressen: rein.map((e) =>
|
||||
brauchtZugang(e)
|
||||
? { ...e, username: zugang.username, credential: zugang.credential }
|
||||
: e),
|
||||
turn_gesamt,
|
||||
turn_bereit: turn_gesamt,
|
||||
grund: "",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user