v6.8.4: the relay password now changes every day
This commit is contained in:
+6
-1
@@ -1,5 +1,6 @@
|
||||
import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
|
||||
import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/iceSettingsStore.js';
|
||||
import { startTurnCredentialRefresh } from './network/turnCredentials.js';
|
||||
import {
|
||||
sessionsReducer,
|
||||
createInitialState,
|
||||
@@ -3861,7 +3862,11 @@ import { GroupCallMedia, mediaErrorCode } from './group/groupCallMedia.js';
|
||||
onClearData: handleClearData
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
// Our relay's credential is minted by the site and expires daily;
|
||||
// fetch one now and keep it renewed. See network/turnCredentials.js.
|
||||
React.useEffect(() => { startTurnCredentialRefresh(); }, []);
|
||||
|
||||
const addMessageWithAutoScroll = React.useCallback((message, type, opts = {}) => {
|
||||
const newId = Date.now() + Math.random();
|
||||
const newMessage = {
|
||||
|
||||
@@ -14104,9 +14104,33 @@ async processMessage(data) {
|
||||
this._trackActiveTimer(r.retryTimer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hand the peer connection the ICE server list as it is NOW.
|
||||
*
|
||||
* A connection keeps the servers it was built with, and our relay's credential
|
||||
* expires after a day (src/network/turnCredentials.js renews it in place on the
|
||||
* shared list). A restart gathers new relay candidates, so it must present the
|
||||
* current credential or a long-lived session would lose its relay path exactly
|
||||
* when it needs one. Best effort: an engine that refuses the update restarts
|
||||
* with what it had.
|
||||
*/
|
||||
_applyCurrentIceServers(pc) {
|
||||
try {
|
||||
if (!pc || typeof pc.getConfiguration !== 'function' || typeof pc.setConfiguration !== 'function') return;
|
||||
const servers = this._config?.webrtc?.iceServers;
|
||||
if (!Array.isArray(servers) || servers.length === 0) return;
|
||||
pc.setConfiguration({ ...pc.getConfiguration(), iceServers: servers });
|
||||
} catch (error) {
|
||||
this._secureLog('warn', 'Could not refresh ICE servers before restart', {
|
||||
errorType: error?.constructor?.name || 'Unknown'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _sendIceRestartOffer() {
|
||||
const pc = this.peerConnection;
|
||||
if (!pc) return;
|
||||
this._applyCurrentIceServers(pc);
|
||||
|
||||
// Rolling back to 'stable' first: a previous restart round-trip may have
|
||||
// left a local offer pending that was never answered.
|
||||
@@ -14189,6 +14213,7 @@ async processMessage(data) {
|
||||
}
|
||||
this._reconnect.phase = 'restarting';
|
||||
|
||||
this._applyCurrentIceServers(pc);
|
||||
await pc.setRemoteDescription({ type: 'offer', sdp: data.sdp });
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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 "<expiry>:<label>"; refuse one already expired.
|
||||
const expiry = Number(String(server.username).split(':')[0]);
|
||||
if (!Number.isFinite(expiry) || expiry <= nowSeconds) return null;
|
||||
const ttl = Number(body.ttl);
|
||||
return {
|
||||
username: server.username,
|
||||
credential: server.credential,
|
||||
expiry,
|
||||
ttl: Number.isFinite(ttl) && ttl > 0 ? ttl : expiry - nowSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
let started = false;
|
||||
|
||||
/**
|
||||
* Start keeping the relay credential fresh. Safe to call more than once.
|
||||
* @param {object} [opts]
|
||||
* @param {() => Array} [opts.getList] the ICE list to update
|
||||
*/
|
||||
export function startTurnCredentialRefresh(opts = {}) {
|
||||
if (started || typeof window === 'undefined' || typeof fetch !== 'function') return;
|
||||
started = true;
|
||||
|
||||
const getList = opts.getList || (() => window.SECUREBIT_ICE_SERVERS);
|
||||
let expiry = 0;
|
||||
let ttl = 0;
|
||||
let timer = null;
|
||||
let failures = 0;
|
||||
|
||||
const schedule = (ms) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(refresh, ms);
|
||||
};
|
||||
|
||||
async function refresh() {
|
||||
timer = null;
|
||||
try {
|
||||
const res = await fetch(ENDPOINT, { method: 'POST', cache: 'no-store', credentials: 'omit' });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const cred = parseCredentialResponse(await res.json(), Date.now() / 1000);
|
||||
if (!cred) throw new Error('unusable response');
|
||||
applyTurnCredentials(getList(), cred);
|
||||
expiry = cred.expiry;
|
||||
ttl = cred.ttl;
|
||||
failures = 0;
|
||||
// Renew at half-life, so a call started just before renewal still has
|
||||
// hours of validity left for the relay to refresh its allocation.
|
||||
schedule(Math.max(60, ttl / 2) * 1000);
|
||||
} catch (_) {
|
||||
const delay = RETRY_DELAYS_MS[Math.min(failures, RETRY_DELAYS_MS.length - 1)];
|
||||
failures++;
|
||||
schedule(delay);
|
||||
}
|
||||
}
|
||||
|
||||
// Timers are throttled in background tabs and stop while a laptop sleeps; on
|
||||
// coming back, renew straight away if the credential is past its half-life.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
const now = Date.now() / 1000;
|
||||
if (!expiry || expiry - now < ttl / 2) refresh();
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
Reference in New Issue
Block a user