chat.html: fetch ephemeral TURN credentials from /turn-cred

Drop the hardcoded shared TURN login; fetch time-limited per-client credentials
from cors-proxy.uncloseai.com/turn-cred before each connection and build
iceServers from them. Falls back to STUN-only (direct path still works) if the
fetch fails.
This commit is contained in:
Russell Ballestrini 2026-05-28 13:27:31 -04:00
parent 5077e982fe
commit cc4cc8065c
No known key found for this signature in database

View file

@ -394,20 +394,32 @@ const PBKDF2_ITER = 600000;
const LOOPBACK = new URLSearchParams(location.search).has('loopback'); const LOOPBACK = new URLSearchParams(location.search).has('loopback');
const RTC_CONFIG = { /* STUN is free + needs no auth; TURN credentials are fetched fresh per
iceServers: [ * connection from /turn-cred (time-limited HMAC, so each client gets its own
/* our STUN/TURN on proxy.uncloseai.com — coturn deployed via * coturn quota instead of sharing one static login). rtcConfig starts STUN-only
* git.unturf.com/engineering/unturf/proxy.unturf.com. * and is upgraded with TURN once the creds arrive — if the fetch fails we still
* credentials are public by design (baked into a static page); * connect directly / via STUN, just without the relay fallback. */
* rate-limited via coturn quotas, not via secrecy. */ const TURN_CRED_URL = new URLSearchParams(location.search).get('turncred')
{ urls: ['stun:proxy.uncloseai.com:3478', 'stun:stun.l.google.com:19302'] }, || 'https://cors-proxy.uncloseai.com/turn-cred';
{ let rtcConfig = {
urls: 'turn:proxy.uncloseai.com:3478', iceServers: [{ urls: ['stun:proxy.uncloseai.com:3478', 'stun:stun.l.google.com:19302'] }]
username: 'zebra',
credential: '7a4a2b1c8d9e6f5a'
}
]
}; };
async function refreshTurnCred() {
try {
const c = await (await fetch(TURN_CRED_URL, { cache: 'no-store' })).json();
if (c && c.credential && c.uris) {
rtcConfig = {
iceServers: [
{ urls: c.stun || ['stun:proxy.uncloseai.com:3478'] },
{ urls: c.uris, username: c.username, credential: c.credential }
]
};
logLine('sys', 'TURN credentials fetched (relay fallback available)');
}
} catch (e) {
logLine('sys', 'no TURN credentials — direct/STUN only (' + e.message + ')');
}
}
/* ============================================================== * /* ============================================================== *
* tiny utils * * tiny utils *
@ -1132,7 +1144,7 @@ const rtcStatus = $('rtc-status');
function ensurePC() { function ensurePC() {
if (pc) return pc; if (pc) return pc;
if (!carrierOn) throw new Error('start carrier first'); if (!carrierOn) throw new Error('start carrier first');
pc = new RTCPeerConnection(RTC_CONFIG); pc = new RTCPeerConnection(rtcConfig);
for (const tr of outboundStream.getTracks()) pc.addTrack(tr, outboundStream); for (const tr of outboundStream.getTracks()) pc.addTrack(tr, outboundStream);
/* Force a memoryless codec (G.711 PCMU/PCMA, else G.722) ahead of Opus. /* Force a memoryless codec (G.711 PCMU/PCMA, else G.722) ahead of Opus.
* Opus is a perceptual codec that re-quantizes in 20ms frames and smears the * Opus is a perceptual codec that re-quantizes in 20ms frames and smears the
@ -1338,6 +1350,7 @@ async function connectRelay() {
logLine('sys', 'carrier auto-started for relay connect'); logLine('sys', 'carrier auto-started for relay connect');
} catch (e) { relaySet('carrier failed: ' + e.message, 'err'); return; } } catch (e) { relaySet('carrier failed: ' + e.message, 'err'); return; }
} }
await refreshTurnCred(); /* fetch fresh ephemeral TURN creds before the PC */
try { try {
relayKey = await deriveSignalKey(code); relayKey = await deriveSignalKey(code);
const room = await deriveSignalRoom(code); const room = await deriveSignalRoom(code);