Переглянути джерело

Merge branch 'hotfix/bizgaze-only-login' of Sravan/BizGaze_Remote into master

Sravan 5 дні тому
джерело
коміт
0a739ee2fd

+ 44
- 0
deploy/coturn/README.md Переглянути файл

@@ -0,0 +1,44 @@
1
+# Self-hosted TURN (coturn) for BizGaze Connect
2
+
3
+Why: customers behind symmetric NAT / corporate firewalls / VPNs can't form a direct
4
+WebRTC path, so screen share blanks out and disconnects. A TURN relay fixes it. We host
5
+our own coturn on a VM we already own — flat cost, no per-GB billing.
6
+
7
+## 1. VM prerequisites
8
+- A VM with a **public IP** (your data-center VM is fine).
9
+- A DNS A record, e.g. `turn.yourdomain.com` -> that public IP.
10
+- A TLS cert for that name (Let's Encrypt): `certbot certonly --standalone -d turn.yourdomain.com`
11
+
12
+## 2. Open firewall ports (on the VM and any edge firewall)
13
+- `3478/udp` and `3478/tcp`  (STUN/TURN)
14
+- `5349/tcp`                 (TURN over TLS)  — and `443/tcp` if you enable alt-tls
15
+- `49152-65535/udp`          (relay range)
16
+
17
+## 3. Configure
18
+Edit `turnserver.conf`:
19
+- `external-ip=` your VM's public IP
20
+- `static-auth-secret=` a long random string (e.g. `openssl rand -hex 32`)
21
+- `realm=` your domain
22
+- `cert=` / `pkey=` paths to your Let's Encrypt cert
23
+
24
+## 4. Run
25
+```
26
+docker compose up -d        # uses docker-compose.yml here
27
+# or native: apt install coturn; copy this file to /etc/turnserver.conf; enable in /etc/default/coturn; systemctl enable --now coturn
28
+```
29
+
30
+## 5. Point the app at it (production env)
31
+```
32
+TURN_URLS=turn:turn.yourdomain.com:3478,turn:turn.yourdomain.com:3478?transport=tcp,turns:turn.yourdomain.com:5349?transport=tcp
33
+TURN_SECRET=<the same static-auth-secret from turnserver.conf>
34
+TURN_TTL=86400
35
+```
36
+The app's `/api/ice` mints short-lived credentials from `TURN_SECRET` automatically — no
37
+permanent password is exposed, and outsiders can't reuse your relay. Restart the app.
38
+
39
+## 6. Verify
40
+- `GET https://<app>/api/ice` should return a `turn:`/`turns:` entry with a username + credential.
41
+- Test page: https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
42
+  Add your `turns:turn.yourdomain.com:5349?transport=tcp` with the username/credential from
43
+  `/api/ice`; you should see a candidate of type **relay**. If you do, restrictive networks
44
+  are covered.

+ 12
- 0
deploy/coturn/docker-compose.yml Переглянути файл

@@ -0,0 +1,12 @@
1
+# Run coturn on your VM:  docker compose up -d
2
+# host networking is required so the UDP relay port range works without per-port mapping.
3
+services:
4
+  coturn:
5
+    image: coturn/coturn:latest
6
+    container_name: coturn
7
+    restart: unless-stopped
8
+    network_mode: host
9
+    volumes:
10
+      - ./turnserver.conf:/etc/coturn/turnserver.conf:ro
11
+      - /etc/letsencrypt:/etc/letsencrypt:ro   # TLS cert for turns:
12
+    command: ["-c", "/etc/coturn/turnserver.conf"]

+ 45
- 0
deploy/coturn/turnserver.conf Переглянути файл

@@ -0,0 +1,45 @@
1
+# coturn config for BizGaze Connect self-hosted TURN.
2
+# Put this on your VM (public IP) and run via Docker (see docker-compose.yml) or
3
+# native coturn (apt install coturn). Replace every CHANGE_ME / placeholder.
4
+
5
+# --- listening ---
6
+listening-port=3478
7
+tls-listening-port=5349
8
+# If this VM has a spare 443, also exposing TURNS on 443 gives the best traversal
9
+# through strict corporate firewalls (uncomment + ensure nothing else uses 443):
10
+# alt-tls-listening-port=443
11
+
12
+# Public address clients reach. If the VM has a 1:1 NAT, use external-ip=PUBLIC/PRIVATE.
13
+external-ip=CHANGE_ME_PUBLIC_IP
14
+
15
+# Relay port range (open these UDP ports in the firewall too).
16
+min-port=49152
17
+max-port=65535
18
+
19
+# --- auth: time-limited shared-secret credentials (matches the app's TURN_SECRET) ---
20
+use-auth-secret
21
+static-auth-secret=CHANGE_ME_LONG_RANDOM_SECRET
22
+realm=connect.yourdomain.com
23
+
24
+# --- TLS (needed for turns: on 5349/443). Use a real cert for turn.yourdomain.com ---
25
+cert=/etc/letsencrypt/live/turn.yourdomain.com/fullchain.pem
26
+pkey=/etc/letsencrypt/live/turn.yourdomain.com/privkey.pem
27
+
28
+# --- hardening ---
29
+fingerprint
30
+no-cli
31
+no-multicast-peers
32
+no-tcp-relay
33
+# Block relaying to private/internal ranges (prevents your relay being used to reach
34
+# your own LAN / cloud metadata — important SSRF protection):
35
+denied-peer-ip=0.0.0.0-0.255.255.255
36
+denied-peer-ip=10.0.0.0-10.255.255.255
37
+denied-peer-ip=100.64.0.0-100.127.255.255
38
+denied-peer-ip=169.254.0.0-169.254.255.255
39
+denied-peer-ip=172.16.0.0-172.31.255.255
40
+denied-peer-ip=192.168.0.0-192.168.255.255
41
+denied-peer-ip=::1
42
+denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
43
+# Optional: cap per-session bandwidth (bytes/sec) to protect the VM, e.g. 700000 = ~5.6 Mbps
44
+# bps-capacity=0
45
+# total-quota=100

+ 1
- 1
server/public/connect.html Переглянути файл

@@ -68,7 +68,7 @@
68 68
 <script>
69 69
 let ICE={iceServers:[{urls:'stun:stun.l.google.com:19302'}]};
70 70
 const IS_MOBILE=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent||'');
71
-let __icePromise=Promise.resolve();try{__icePromise=fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers&&IS_MOBILE)ICE=c;}).catch(()=>{});}catch(_){}
71
+let __icePromise=Promise.resolve();try{__icePromise=fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers)ICE=c;}).catch(()=>{});}catch(_){}
72 72
 async function ensureIce(){try{await __icePromise;}catch(_){}return ICE;}
73 73
 function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
74 74
 // When embedded in the home shell, tell the parent when a session is live so the

+ 6
- 2
server/public/share.html Переглянути файл

@@ -79,7 +79,7 @@ let ICE={iceServers:[{urls:'stun:stun.l.google.com:19302'}]};
79 79
 let SHARER_NAME='Customer';
80 80
 try{fetch('/api/me').then(r=>r.ok?r.json():null).then(m=>{if(m&&(m.name||m.email))SHARER_NAME=m.name||m.email;}).catch(()=>{});}catch(_){}
81 81
 const IS_MOBILE=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent||'');
82
-let __icePromise=Promise.resolve();try{__icePromise=fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers&&IS_MOBILE)ICE=c;}).catch(()=>{});}catch(_){}
82
+let __icePromise=Promise.resolve();try{__icePromise=fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers)ICE=c;}).catch(()=>{});}catch(_){}
83 83
 async function ensureIce(){try{await __icePromise;}catch(_){}return ICE;}
84 84
 function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
85 85
 // When embedded in the home shell, tell the parent when a session is live so the
@@ -168,11 +168,15 @@ async function startStreaming(){
168 168
   pc.ondatachannel=(ev)=>{ev.channel.onmessage=()=>{};};
169 169
   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]; } };
170 170
   pc.onicecandidate=(ev)=>{if(ev.candidate)ws.send(JSON.stringify({type:'ice-candidate',sessionId,candidate:ev.candidate}));};
171
-  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.'); } };
171
+  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."); } };
172 172
   chatChannel=pc.createDataChannel('chat',{ordered:true});
173 173
   chatChannel.onmessage=(e)=>{try{addChat(JSON.parse(e.data));}catch(_){}};
174 174
   const offer=await pc.createOffer(); await pc.setLocalDescription(offer);
175 175
   ws.send(JSON.stringify({type:'offer',sessionId,sdp:pc.localDescription}));
176
+  // Watchdog: if we can't establish the peer connection in time, show a clear reason
177
+  // instead of a blank screen (covers networks with no usable path even via TURN).
178
+  clearTimeout(window.__connWatch);
179
+  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);
176 180
   localStream.getVideoTracks()[0].onended=()=>{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));teardown();};
177 181
 }
178 182
 const SR = window.SpeechRecognition || window.webkitSpeechRecognition;

+ 15
- 7
server/routes.js Переглянути файл

@@ -122,16 +122,24 @@ route('GET', '/api/setup-state', async (req, res) => {
122 122
   json(res, 200, { registrationOpen: !anyUser || process.env.ALLOW_REGISTRATION === '1' });
123 123
 });
124 124
 
125
-// ICE servers for WebRTC. Always includes a public STUN; adds managed TURN if
126
-// configured via env (TURN_URLS comma-separated, TURN_USERNAME, TURN_CREDENTIAL).
125
+// ICE servers for WebRTC. Always includes a public STUN; adds our TURN relay if
126
+// configured. Two credential modes:
127
+//   - Shared secret (recommended, coturn `use-auth-secret`): set TURN_SECRET and we mint
128
+//     time-limited credentials per request (no permanent password is ever handed out, so
129
+//     outsiders can't reuse your relay). Optional TURN_TTL seconds (default 24h).
130
+//   - Static: set TURN_USERNAME + TURN_CREDENTIAL for a fixed long-term credential.
127 131
 route('GET', '/api/ice', async (req, res) => {
128 132
   const iceServers = [{ urls: 'stun:stun.l.google.com:19302' }];
129 133
   if (process.env.TURN_URLS) {
130
-    iceServers.push({
131
-      urls: process.env.TURN_URLS.split(',').map((u) => u.trim()).filter(Boolean),
132
-      username: process.env.TURN_USERNAME || '',
133
-      credential: process.env.TURN_CREDENTIAL || '',
134
-    });
134
+    const urls = process.env.TURN_URLS.split(',').map((u) => u.trim()).filter(Boolean);
135
+    let username = process.env.TURN_USERNAME || '';
136
+    let credential = process.env.TURN_CREDENTIAL || '';
137
+    if (process.env.TURN_SECRET) {
138
+      const ttl = parseInt(process.env.TURN_TTL || '86400', 10);
139
+      username = String(Math.floor(Date.now() / 1000) + ttl); // coturn expects "<expiry>"
140
+      credential = require('crypto').createHmac('sha1', process.env.TURN_SECRET).update(username).digest('base64');
141
+    }
142
+    iceServers.push({ urls, username, credential });
135 143
   }
136 144
   json(res, 200, { iceServers });
137 145
 });

Завантаження…
Відмінити
Зберегти