// Keeps the credential for SecureBit's own TURN relay fresh. // // The relay's credential is no longer something the client carries: it is minted // by the site (POST /api/turn-credentials, see deploy/turn-credentials.js) and // expires after a day. This module fetches one at startup and again before it // runs out, and writes it into the ICE list the connection managers already hold. // // The update is made IN PLACE on the existing entry objects. Every manager keeps a // reference to window.SECUREBIT_ICE_SERVERS rather than a copy, so the next peer // connection — and the next in-band ICE restart, which re-reads the list — picks // up the new credential without anything else having to know it changed. // // If the endpoint cannot be reached, nothing is changed and the entry keeps the // credential it shipped with. Only our relay's entries are touched; a TURN server // the user configured themselves is never modified. const ENDPOINT = '/api/turn-credentials'; const OWN_RELAY_HOSTS = ['turn.securebit.chat', '144.172.96.126']; const MAX_FIELD = 512; // How long to wait before trying again after a failed fetch. const RETRY_DELAYS_MS = [60_000, 5 * 60_000, 15 * 60_000]; function relayHost(url) { const m = /^turns?:([^:?\s]+)/i.exec(String(url || '').trim()); return m ? m[1].toLowerCase() : null; } /** True when an ICE entry points at SecureBit's own relay. */ export function isOwnRelayEntry(entry) { if (!entry || typeof entry !== 'object') return false; const urls = Array.isArray(entry.urls) ? entry.urls : [entry.urls]; return urls.some((u) => OWN_RELAY_HOSTS.includes(relayHost(u))); } function isCleanField(value) { if (typeof value !== 'string' || value.length === 0 || value.length > MAX_FIELD) return false; for (let i = 0; i < value.length; i++) { const c = value.charCodeAt(i); if (c < 0x20 || c === 0x7f) return false; } return true; } /** * Write a fresh credential into every own-relay entry of `list`, in place. * @returns {boolean} whether anything was updated */ export function applyTurnCredentials(list, cred) { if (!Array.isArray(list) || !cred || !isCleanField(cred.username) || !isCleanField(cred.credential)) { return false; } let updated = false; for (const entry of list) { if (!isOwnRelayEntry(entry)) continue; entry.username = cred.username; entry.credential = cred.credential; updated = true; } return updated; } /** Pull the credential out of an endpoint response, or null if it is not usable. */ export function parseCredentialResponse(body, nowSeconds) { const server = body && Array.isArray(body.iceServers) ? body.iceServers[0] : null; if (!server || !isCleanField(server.username) || !isCleanField(server.credential)) return null; // coturn REST-API usernames are ":