OASIS_MOBILE/src/backend/turnCredentials.js
SITO 841acf2a7a karvan: quitar el STUN de Google y servir la configuracion ICE del propio nodo
El cliente tenia stun.l.google.com fijo en el codigo. Un servidor STUN aprende la
IP publica y el momento de cada consulta, asi que tal cual estaba cada llamada le
avisaba a Google de que ese usuario esta llamando. En un proyecto con esta postura
sobre privacidad no se sostiene.

Por defecto ya no hay ningun servidor ICE: solo candidatos host, que funcionan en
la misma red y con NAT amable. Es mas honesto degradar a nada que degradar a un
tercero.

Se anade turnCredentials.js, siguiendo el estilo de los helpers de src/backend/:
genera credenciales efimeras con el mecanismo REST de coturn (use-auth-secret),
username = caducidad:identificador y credential = HMAC-SHA1 en base64. El feed id
hace de identificador, que es lo que distingue a quien llama en Oasis. Como coturn
valida el HMAC, las credenciales se emiten sin hablar con el servidor y caducan
solas.

La ruta GET /karvan/ice las sirve solo a loopback —la credencial va firmada con el
secreto del nodo y no debe salir de la maquina— y el cliente la pide ANTES de crear
ninguna RTCPeerConnection, porque la configuracion ICE de una conexion ya creada no
se puede cambiar de forma fiable. Con dos segundos de espera maxima y respaldo a
candidatos host.

La configuracion vive en oasis-config.json bajo rtc, vacia de fabrica, y admite tres
formas: STUN propio, TURN con secreto compartido, o credenciales estaticas. Con
relayOnly se fuerza iceTransportPolicy relay para que ningun par vea la IP del otro.

Verificado: sin configuracion devuelve iceServers vacio; con secreto emite credencial
reproducible y con caducidad; cero referencias a Google en el codigo.
2026-08-19 00:16:50 +02:00

50 lines
2.2 KiB
JavaScript

"use strict";
// Credenciales efimeras para un TURN propio (coturn con use-auth-secret).
//
// Es el mecanismo REST de coturn: el servidor no guarda usuarios, valida que
// credential == base64(HMAC-SHA1(static-auth-secret, username))
// y que el timestamp del username no haya caducado. Asi las credenciales se
// generan sin hablar con coturn y caducan solas.
//
// Portado del turn.js de karvan-web, cambiando sessionId por el feed id: lo que
// identifica a quien llama en Oasis es su feed, no una sesion anonima.
const crypto = require("crypto");
const DEFAULT_TTL = 3600;
// username = <caduca en unix> ":" <identificador>
function mintTurnCredentials({ urls, secret, ttlSec = DEFAULT_TTL }, who, now = Date.now()) {
if (!secret || !Array.isArray(urls) || !urls.length) return null;
const expiry = Math.floor(now / 1000) + ttlSec;
const username = expiry + ":" + String(who || "anon").slice(0, 64);
const credential = crypto.createHmac("sha1", secret).update(username).digest("base64");
return { urls, username, credential, ttl: ttlSec, expiry };
}
// Lee la configuracion de rtc y devuelve lo que el navegador espera en iceServers.
// Formas admitidas en oasis-config.json:
// "rtc": { "stun": ["stun:mi.pub:3478"] }
// "rtc": { "turn": { "urls": [...], "secret": "...", "ttlSec": 3600 }, "relayOnly": true }
// "rtc": { "turn": { "urls": [...], "username": "...", "credential": "..." } } (estatico)
function buildIceServers(rtc, who, now = Date.now()) {
const out = [];
if (!rtc || typeof rtc !== "object") return { iceServers: out, relayOnly: false };
if (Array.isArray(rtc.stun)) {
for (const u of rtc.stun) if (typeof u === "string" && u) out.push({ urls: u });
}
const t = rtc.turn;
if (t && Array.isArray(t.urls) && t.urls.length) {
if (t.secret) {
const c = mintTurnCredentials(t, who, now);
if (c) out.push({ urls: c.urls, username: c.username, credential: c.credential });
} else if (t.username && t.credential) {
out.push({ urls: t.urls, username: t.username, credential: t.credential });
}
}
return { iceServers: out, relayOnly: !!rtc.relayOnly && out.some(s => s.credential) };
}
module.exports = { mintTurnCredentials, buildIceServers };