diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index b0aa02e..4590008 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -30,7 +30,15 @@ export const config = { minPasswordLength: 8, /** Maximum registrations allowed from the same IP (permanent, DB-level) */ maxRegistrationsPerIp: Number(process.env.MAX_REGISTRATIONS_PER_IP) || 2, + /** TURN server URL (e.g. turn:openrelay.metered.ca:80) */ + turnUrl: process.env.TURN_URL || '', + /** FOR DYNAMIC AUTH: Shared secret (coturn / metered.ca) */ + turnSecret: process.env.TURN_SECRET || '', + /** FOR STATIC AUTH: Username */ + turnUsername: process.env.TURN_USERNAME || '', + /** FOR STATIC AUTH: Password */ + turnPassword: process.env.TURN_PASSWORD || '', /** STUN server URLs */ - stunUrls: (process.env.STUN_URLS || 'stun:stun.yandex.ru:3478,stun:stun.sipnet.net:3478,stun:stun.l.google.com:19302,stun:stun.voip.aebc.com:3478,stun:stun.stunprotocol.org:3478') + stunUrls: (process.env.STUN_URLS || 'stun:stun.yandex.ru:3478,stun:stun.mail.ru:3478,stun:stun.sipnet.net:3478,stun:stun.l.google.com:19302,stun:stun.voip.aebc.com:3478') .split(',').map(s => s.trim()).filter(Boolean), }; diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index b0cea86..ed021f3 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -124,6 +124,35 @@ app.get('/api/ice-servers', authenticateToken, (_req: AuthRequest, res) => { iceServers.push({ urls: config.stunUrls }); } + // TURN сервер (динамическая или статическая аутентификация) + if (config.turnUrl) { + const urls = config.turnUrl.split(',').map(s => s.trim()); + + if (config.turnSecret) { + // Динамический пароль (HMAC-SHA1) + const ttl = 48 * 3600; + const timestamp = Math.floor(Date.now() / 1000) + ttl; + const username = `${timestamp}:vortex`; + const credential = crypto + .createHmac('sha1', config.turnSecret) + .update(username) + .digest('base64'); + + iceServers.push({ + urls: urls, + username, + credential, + }); + } else if (config.turnUsername && config.turnPassword) { + // Статичный логин/пароль + iceServers.push({ + urls: urls, + username: config.turnUsername, + credential: config.turnPassword, + }); + } + } + res.json({ iceServers }); }); diff --git a/apps/web/src/components/CallModal.tsx b/apps/web/src/components/CallModal.tsx index 4a1f1e5..9c71be3 100644 --- a/apps/web/src/components/CallModal.tsx +++ b/apps/web/src/components/CallModal.tsx @@ -319,6 +319,13 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi setCallState('calling'); try { + // Security context check (critical for KeenDNS) + if (!window.isSecureContext) { + alert('Критическая ошибка: Совершение аудио/видео звонков требует безопасного соединения (HTTPS).\n\nЗайдите в настройки KeenDNS и убедитесь, что включено HTTPS (SSL), а в браузере используйте https:// вместо http://'); + setCallState('ended'); + return; + } + // Get media (video with camera enumeration fallback) console.log('[startCall] Getting media, wantVideo:', callType === 'video'); const { stream, hasVideo } = await getMediaWithCameraFallback(callType === 'video'); diff --git a/docker-compose.yml b/docker-compose.yml index 5d99429..2e2e744 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,9 @@ services: JWT_SECRET: ${JWT_SECRET:-your-secure-random-secret-key-here} CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost,http://localhost:80,http://localhost:9090,http://localhost:5173,http://${DOMAIN:-mess.khomegeneric.keenetic.pro},https://${DOMAIN:-mess.khomegeneric.keenetic.pro}} ENCRYPTION_KEY: ${ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef} + TURN_URL: ${TURN_URL:-} + TURN_USERNAME: ${TURN_USERNAME:-} + TURN_PASSWORD: ${TURN_PASSWORD:-} depends_on: - db ports: