feat(turn): self-hosted coturn support + time-limited creds + failure UX
- /api/ice: when TURN_SECRET is set, mint short-lived HMAC credentials (coturn use-auth-secret) so no permanent password is exposed and the relay can't be abused. Static TURN_USERNAME/CREDENTIAL still supported. - share.html: connection watchdog + clear "couldn't connect on this network" message instead of a blank screen when no path can be established. - deploy/coturn: ready-to-run turnserver.conf + docker-compose + README for hosting our own TURN on a VM we own (flat cost, no per-GB billing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -168,11 +168,15 @@ async function startStreaming(){
|
||||
pc.ondatachannel=(ev)=>{ev.channel.onmessage=()=>{};};
|
||||
pc.ontrack=(ev)=>{ if(ev.track.kind==='audio'){ let a=document.getElementById('remoteAudio'); if(!a){a=document.createElement('audio');a.id='remoteAudio';a.autoplay=true;document.body.appendChild(a);} a.srcObject=ev.streams[0]; } };
|
||||
pc.onicecandidate=(ev)=>{if(ev.candidate)ws.send(JSON.stringify({type:'ice-candidate',sessionId,candidate:ev.candidate}));};
|
||||
pc.onconnectionstatechange=()=>{ if(pc&&pc.connectionState==='failed'){ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));}catch(_){} endShareSession('The connection was lost. Tap below for a new code if you still need help.'); } };
|
||||
pc.onconnectionstatechange=()=>{ if(!pc) return; if(pc.connectionState==='connected'){ clearTimeout(window.__connWatch); } if(pc.connectionState==='failed'){ clearTimeout(window.__connWatch); try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));}catch(_){} endShareSession("Couldn't connect on this network — it may be blocking screen sharing. Try a different network (e.g. mobile data / hotspot), then tap below for a new code."); } };
|
||||
chatChannel=pc.createDataChannel('chat',{ordered:true});
|
||||
chatChannel.onmessage=(e)=>{try{addChat(JSON.parse(e.data));}catch(_){}};
|
||||
const offer=await pc.createOffer(); await pc.setLocalDescription(offer);
|
||||
ws.send(JSON.stringify({type:'offer',sessionId,sdp:pc.localDescription}));
|
||||
// Watchdog: if we can't establish the peer connection in time, show a clear reason
|
||||
// instead of a blank screen (covers networks with no usable path even via TURN).
|
||||
clearTimeout(window.__connWatch);
|
||||
window.__connWatch=setTimeout(()=>{ if(pc && pc.connectionState!=='connected' && !sessionOver){ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));}catch(_){} endShareSession("Couldn't connect on this network — it may be blocking screen sharing. Try a different network (e.g. mobile data / hotspot), then tap below for a new code."); } }, 20000);
|
||||
localStream.getVideoTracks()[0].onended=()=>{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));teardown();};
|
||||
}
|
||||
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
|
||||
+15
-7
@@ -122,16 +122,24 @@ route('GET', '/api/setup-state', async (req, res) => {
|
||||
json(res, 200, { registrationOpen: !anyUser || process.env.ALLOW_REGISTRATION === '1' });
|
||||
});
|
||||
|
||||
// ICE servers for WebRTC. Always includes a public STUN; adds managed TURN if
|
||||
// configured via env (TURN_URLS comma-separated, TURN_USERNAME, TURN_CREDENTIAL).
|
||||
// ICE servers for WebRTC. Always includes a public STUN; adds our TURN relay if
|
||||
// configured. Two credential modes:
|
||||
// - Shared secret (recommended, coturn `use-auth-secret`): set TURN_SECRET and we mint
|
||||
// time-limited credentials per request (no permanent password is ever handed out, so
|
||||
// outsiders can't reuse your relay). Optional TURN_TTL seconds (default 24h).
|
||||
// - Static: set TURN_USERNAME + TURN_CREDENTIAL for a fixed long-term credential.
|
||||
route('GET', '/api/ice', async (req, res) => {
|
||||
const iceServers = [{ urls: 'stun:stun.l.google.com:19302' }];
|
||||
if (process.env.TURN_URLS) {
|
||||
iceServers.push({
|
||||
urls: process.env.TURN_URLS.split(',').map((u) => u.trim()).filter(Boolean),
|
||||
username: process.env.TURN_USERNAME || '',
|
||||
credential: process.env.TURN_CREDENTIAL || '',
|
||||
});
|
||||
const urls = process.env.TURN_URLS.split(',').map((u) => u.trim()).filter(Boolean);
|
||||
let username = process.env.TURN_USERNAME || '';
|
||||
let credential = process.env.TURN_CREDENTIAL || '';
|
||||
if (process.env.TURN_SECRET) {
|
||||
const ttl = parseInt(process.env.TURN_TTL || '86400', 10);
|
||||
username = String(Math.floor(Date.now() / 1000) + ttl); // coturn expects "<expiry>"
|
||||
credential = require('crypto').createHmac('sha1', process.env.TURN_SECRET).update(username).digest('base64');
|
||||
}
|
||||
iceServers.push({ urls, username, credential });
|
||||
}
|
||||
json(res, 200, { iceServers });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user