diff --git a/meet/server/package.json b/meet/server/package.json index c4bf44f8..4787209f 100644 --- a/meet/server/package.json +++ b/meet/server/package.json @@ -1,35 +1,38 @@ { "name": "kolabmeet-server", "version": "3.3.4", "private": true, "license": "MIT", "scripts": { "start": "node server.js", "connect": "node connect.js", "lint": "eslint -c .eslintrc.json --ext .js *.js lib/" }, "dependencies": { "awaitqueue": "^1.0.0", "axios": "^0.21.1", "body-parser": "^1.19.0", "colors": "^1.4.0", "compression": "^1.7.4", "connect-redis": "^4.0.3", "cookie-parser": "^1.4.4", "debug": "^4.1.1", "express": "^4.17.1", "express-session": "^1.17.0", "express-socket.io-session": "^1.3.5", "helmet": "^3.21.2", "mediasoup": "^3.5.14", "pidusage": "^2.0.17", "prom-client": ">=12.0.0", "redis": "^2.8.0", "socket.io": "^2.3.0", "spdy": "^4.0.1", "uuid": "^7.0.2" }, "devDependencies": { - "eslint": "^6.8.0" + "eslint": "^6.8.0", + "mediasoup-client": "^3.6.37", + "mocha": "^9.1.1", + "supertest": "^6.1.6" } } diff --git a/meet/server/server.js b/meet/server/server.js index 3b23b474..17b7abc5 100755 --- a/meet/server/server.js +++ b/meet/server/server.js @@ -1,420 +1,423 @@ #!/usr/bin/env node process.title = 'kolabmeet-server'; const config = require('./config/config'); const fs = require('fs'); const http = require('http'); const spdy = require('spdy'); const express = require('express'); const bodyParser = require('body-parser'); const cookieParser = require('cookie-parser'); const compression = require('compression'); const mediasoup = require('mediasoup'); const AwaitQueue = require('awaitqueue'); const Logger = require('./lib/Logger'); const Room = require('./lib/Room'); const Peer = require('./lib/Peer'); const helmet = require('helmet'); // auth const redis = require('redis'); -const redisClient = redis.createClient(config.redisOptions); const expressSession = require('express-session'); const RedisStore = require('connect-redis')(expressSession); const sharedSession = require('express-socket.io-session'); const interactiveServer = require('./lib/interactiveServer'); const promExporter = require('./lib/promExporter'); const { v4: uuidv4 } = require('uuid'); /* eslint-disable no-console */ console.log('- process.env.DEBUG:', process.env.DEBUG); console.log('- config.mediasoup.worker.logLevel:', config.mediasoup.worker.logLevel); console.log('- config.mediasoup.worker.logTags:', config.mediasoup.worker.logTags); /* eslint-enable no-console */ const logger = new Logger(); const queue = new AwaitQueue(); let statusLogger = null; if ('StatusLogger' in config) statusLogger = new config.StatusLogger(); // mediasoup Workers. // @type {Array} const mediasoupWorkers = []; // Map of Room instances indexed by roomId. const rooms = new Map(); // Map of Peer instances indexed by peerId. const peers = new Map(); // TLS server configuration. const tls = { cert : fs.readFileSync(config.tls.cert), key : fs.readFileSync(config.tls.key), secureOptions : 'tlsv12', ciphers : [ 'ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305', 'DHE-RSA-AES128-GCM-SHA256', 'DHE-RSA-AES256-GCM-SHA384' ].join(':'), honorCipherOrder : true }; const app = express(); app.use(helmet.hsts()); const sharedCookieParser=cookieParser(); app.use(sharedCookieParser); app.use(bodyParser.json({ limit: '5mb' })); app.use(bodyParser.urlencoded({ limit: '5mb', extended: true })); const session = expressSession({ secret : config.cookieSecret, name : config.cookieName, resave : true, saveUninitialized : true, - store : new RedisStore({ client: redisClient }), + store : config.redisOptions.host != 'none' ? new RedisStore({ client: redis.createClient(config.redisOptions) }) : null, cookie : { secure : true, httpOnly : true, maxAge : 60 * 60 * 1000 // Expire after 1 hour since last request from user } }); if (config.trustProxy) { app.set('trust proxy', config.trustProxy); } app.use(session); let mainListener; let io; async function run() { try { // Open the interactive server. await interactiveServer(rooms, peers); // start Prometheus exporter if (config.prometheus) { await promExporter(rooms, peers, config.prometheus); } // Run a mediasoup Worker. await runMediasoupWorkers(); // Run HTTPS server. await runHttpsServer(); // Run WebSocketServer. await runWebSocketServer(); // eslint-disable-next-line no-unused-vars const errorHandler = (err, req, res, next) => { const trackingId = uuidv4(); res.status(500).send( `

Internal Server Error

If you report this error, please also report this tracking ID which makes it possible to locate your session in the logs which are available to the system administrator: ${trackingId}

` ); logger.error( 'Express error handler dump with tracking ID: %s, error dump: %o', trackingId, err); }; // eslint-disable-next-line no-unused-vars app.use(errorHandler); } catch (error) { logger.error('run() [error:"%o"]', error); } + + app.emit('ready'); } function statusLog() { if (statusLogger) { statusLogger.log({ rooms : rooms, peers : peers }); } } async function runHttpsServer() { app.use(compression()); app.get(`${config.pathPrefix}/api/ping`, function (req, res, /*next*/) { res.send('PONG') }) app.get(`${config.pathPrefix}/api/sessions`, function (req, res, /*next*/) { //TODO json.stringify res.json({ id : "testId" }) }) //Check if the room exists app.get(`${config.pathPrefix}/api/sessions/:session_id`, function (req, res, /*next*/) { console.warn("Checking for room") let room = rooms.get(req.params.session_id); if (!room) { console.warn("doesn't exist") res.status(404).send() } else { console.warn("exist") res.status(200).send() } }) // Create room and return id app.post(`${config.pathPrefix}/api/sessions`, async function (req, res, /*next*/) { console.warn("Creating new room", req.body.mediaMode, req.body.recordingMode) //FIXME we're truncating because of kolab4 database layout (should be fixed instead) const roomId = uuidv4().substring(0, 16) await getOrCreateRoom({ roomId }); res.json({ id : roomId }) }) app.post(`${config.pathPrefix}/api/signal`, async function (req, res, /*next*/) { let data = req.body; const roomId = data.session; // const signalType = data.type; // const payload = data.data; const peers = data.to; if (peers) { for (const peerId of peers) { let peer = peers.get(peerId); peer.socket.emit( 'signal', data ); } } else { io.to(roomId).emit( 'signal', data ); } res.json({}) }); // Create connection in room (just wait for websocket instead? // $post = [ // 'json' => [ // 'role' => self::OV_ROLE_PUBLISHER, // 'data' => json_encode(['role' => $role]) // ] // ]; app.post(`${config.pathPrefix}/api/sessions/:session_id/connection`, function (req, res, /*next*/) { console.warn("Creating connection in session", req.params.session_id) let roomId = req.params.session_id let data = req.body; //FIXME we're truncating because of kolab4 database layout (should be fixed instnead) const peerId = uuidv4().substring(0, 16) //TODO create room already? let peer = new Peer({ id: peerId, roomId }); peers.set(peerId, peer); peer.on('close', () => { peers.delete(peerId); statusLog(); }); peer.nickname = "Display Name"; // peer.picture = picture; peer.email = "email@test.com"; if ('role' in data) peer.setRole(data.role); const proto = config.publicDomain.includes('localhost') || config.publicDomain.includes('127.0.0.1') ? 'ws' : 'wss'; res.json({ id: peerId, // When the below get's passed to the socket.io client we end up with something like (depending on the socket.io path) // wss://${publicDomain}/meetmedia/signaling/?peerId=peer1&roomId=room1&EIO=3&transport=websocket, token: `${proto}://${config.publicDomain}/?peerId=${peerId}&roomId=${roomId}` }) }) if (config.httpOnly === true) { // http mainListener = http.createServer(app); } else { // https mainListener = spdy.createServer(tls, app); // http const redirectListener = http.createServer(app); if (config.listeningHost) redirectListener.listen(config.listeningRedirectPort, config.listeningHost); else redirectListener.listen(config.listeningRedirectPort); } console.info(`Listening on ${config.listeningPort} ${config.listeningHost}`) // https or http if (config.listeningHost) mainListener.listen(config.listeningPort, config.listeningHost); else mainListener.listen(config.listeningPort); } /** * Create a WebSocketServer to allow WebSocket connections from browsers. */ async function runWebSocketServer() { io = require('socket.io')(mainListener, { path: `${config.pathPrefix}/signaling`, cookie: false }); io.use( sharedSession(session, sharedCookieParser, { autoSave: true }) ); // Handle connections from clients. io.on('connection', (socket) => { logger.info("websocket connection") const { roomId, peerId } = socket.handshake.query; if (!roomId || !peerId) { logger.warn('connection request without roomId and/or peerId'); socket.disconnect(true); return; } logger.info( 'connection request [roomId:"%s", peerId:"%s"]', roomId, peerId); queue.push(async () => { const room = await getOrCreateRoom({ roomId }); let peer = peers.get(peerId); if (!peer) { logger.warn("Peer does not exist %s", peerId); socket.disconnect(true); return; } peer.socket = socket; room.handlePeer({ peer }); statusLog(); }) .catch((error) => { logger.error('room creation or room joining failed [error:"%o"]', error); if (socket) socket.disconnect(true); return; }); }); } /** * Launch as many mediasoup Workers as given in the configuration file. */ async function runMediasoupWorkers() { const { numWorkers } = config.mediasoup; logger.info('running %d mediasoup Workers...', numWorkers); for (let i = 0; i < numWorkers; ++i) { const worker = await mediasoup.createWorker( { logLevel : config.mediasoup.worker.logLevel, logTags : config.mediasoup.worker.logTags, rtcMinPort : config.mediasoup.worker.rtcMinPort, rtcMaxPort : config.mediasoup.worker.rtcMaxPort }); worker.on('died', () => { logger.error( 'mediasoup Worker died, exiting in 2 seconds... [pid:%d]', worker.pid); setTimeout(() => process.exit(1), 2000); }); mediasoupWorkers.push(worker); } } /** * Get a Room instance (or create one if it does not exist). */ async function getOrCreateRoom({ roomId }) { let room = rooms.get(roomId); // If the Room does not exist create a new one. if (!room) { logger.info('creating a new Room [roomId:"%s"]', roomId); room = await Room.create({ mediasoupWorkers, roomId, peers }); rooms.set(roomId, room); statusLog(); room.on('close', () => { rooms.delete(roomId); statusLog(); }); } return room; } run(); + +module.exports = app; // export for testing diff --git a/meet/server/test/fakeParameters.js b/meet/server/test/fakeParameters.js new file mode 100644 index 00000000..dd747213 --- /dev/null +++ b/meet/server/test/fakeParameters.js @@ -0,0 +1,619 @@ +const uuidv4 = require('uuid/v4'); + +exports.generateRouterRtpCapabilities = function() +{ + return { + codecs : + [ + { + mimeType : 'audio/opus', + kind : 'audio', + preferredPayloadType : 100, + clockRate : 48000, + channels : 2, + rtcpFeedback : + [ + { type: 'transport-cc' } + ], + parameters : + { + useinbandfec : 1, + foo : 'bar' + } + }, + { + mimeType : 'video/VP8', + kind : 'video', + preferredPayloadType : 101, + clockRate : 90000, + rtcpFeedback : + [ + { type: 'nack' }, + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'goog-remb' }, + { type: 'transport-cc' } + ], + parameters : + { + 'x-google-start-bitrate' : 1500 + } + }, + { + mimeType : 'video/rtx', + kind : 'video', + preferredPayloadType : 102, + clockRate : 90000, + rtcpFeedback : [], + parameters : + { + apt : 101 + } + }, + { + mimeType : 'video/H264', + kind : 'video', + preferredPayloadType : 103, + clockRate : 90000, + rtcpFeedback : + [ + { type: 'nack' }, + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'goog-remb' }, + { type: 'transport-cc' } + ], + parameters : + { + 'level-asymmetry-allowed' : 1, + 'packetization-mode' : 1, + 'profile-level-id' : '42e01f' + } + }, + { + mimeType : 'video/rtx', + kind : 'video', + preferredPayloadType : 104, + clockRate : 90000, + rtcpFeedback : [], + parameters : + { + apt : 103 + } + } + ], + headerExtensions : + [ + { + kind : 'audio', + uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId : 1, + preferredEncrypt : false, + direction : 'sendrecv' + }, + { + kind : 'video', + uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId : 1, + preferredEncrypt : false, + direction : 'sendrecv' + }, + { + kind : 'video', + uri : 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', + preferredId : 2, + preferredEncrypt : false, + direction : 'recvonly' + }, + { + kind : 'video', + uri : 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id', + preferredId : 3, + preferredEncrypt : false, + direction : 'recvonly' + }, + { + kind : 'audio', + uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + preferredId : 4, + preferredEncrypt : false, + direction : 'sendrecv' + }, + { + kind : 'video', + uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + preferredId : 4, + preferredEncrypt : false, + direction : 'sendrecv' + }, + { + kind : 'audio', + uri : 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + preferredId : 5, + preferredEncrypt : false, + direction : 'recvonly' + }, + { + kind : 'video', + uri : 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + preferredId : 5, + preferredEncrypt : false, + direction : 'sendrecv' + }, + { + kind : 'video', + uri : 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07', + preferredId : 6, + preferredEncrypt : false, + direction : 'sendrecv' + }, + { + kind : 'video', + uri : 'urn:ietf:params:rtp-hdrext:framemarking', + preferredId : 7, + preferredEncrypt : false, + direction : 'sendrecv' + }, + { + kind : 'audio', + uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + preferredId : 10, + preferredEncrypt : false, + direction : 'sendrecv' + }, + { + kind : 'video', + uri : 'urn:3gpp:video-orientation', + preferredId : 11, + preferredEncrypt : false, + direction : 'sendrecv' + }, + { + kind : 'video', + uri : 'urn:ietf:params:rtp-hdrext:toffset', + preferredId : 12, + preferredEncrypt : false, + direction : 'sendrecv' + } + ], + fecMechanisms : [] + }; +}; + +exports.generateNativeRtpCapabilities = function() +{ + return { + codecs : + [ + { + mimeType : 'audio/opus', + kind : 'audio', + preferredPayloadType : 111, + clockRate : 48000, + channels : 2, + rtcpFeedback : + [ + { type: 'transport-cc' } + ], + parameters : + { + minptime : 10, + useinbandfec : 1 + } + }, + { + mimeType : 'audio/ISAC', + kind : 'audio', + preferredPayloadType : 103, + clockRate : 16000, + channels : 1, + rtcpFeedback : + [ + { type: 'transport-cc' } + ], + parameters : {} + }, + { + mimeType : 'audio/CN', + kind : 'audio', + preferredPayloadType : 106, + clockRate : 32000, + channels : 1, + rtcpFeedback : + [ + { type: 'transport-cc' } + ], + parameters : {} + }, + { + mimeType : 'video/VP8', + kind : 'video', + preferredPayloadType : 96, + clockRate : 90000, + rtcpFeedback : + [ + { type: 'goog-remb' }, + { type: 'transport-cc' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'nack' }, + { type: 'nack', parameter: 'pli' } + ], + parameters : + { + baz : '1234abcd' + } + }, + { + mimeType : 'video/rtx', + kind : 'video', + preferredPayloadType : 97, + clockRate : 90000, + rtcpFeedback : [], + parameters : + { + apt : 96 + } + } + ], + headerExtensions : + [ + { + kind : 'audio', + uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId : 1 + }, + { + kind : 'video', + uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId : 1 + }, + { + kind : 'video', + uri : 'urn:ietf:params:rtp-hdrext:toffset', + preferredId : 2 + }, + { + kind : 'video', + uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + preferredId : 3 + }, + { + kind : 'video', + uri : 'urn:3gpp:video-orientation', + preferredId : 4 + }, + { + kind : 'video', + uri : 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + preferredId : 5 + }, + { + kind : 'video', + uri : 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', + preferredId : 6 + }, + { + kind : 'video', + uri : 'http://www.webrtc.org/experiments/rtp-hdrext/video-content-type', + preferredId : 7 + }, + { + kind : 'video', + uri : 'http://www.webrtc.org/experiments/rtp-hdrext/video-timing', + preferredId : 8 + }, + { + kind : 'audio', + uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + preferredId : 10 + } + ], + fecMechanisms : [] + }; +}; + +exports.generateNativeSctpCapabilities = function() +{ + return { + numStreams : { OS: 2048, MIS: 2048 } + }; +}; + +exports.generateLocalDtlsParameters = function() +{ + return { + fingerprints : + [ + { + algorithm : 'sha-256', + value : '82:5A:68:3D:36:C3:0A:DE:AF:E7:32:43:D2:88:83:57:AC:2D:65:E5:80:C4:B6:FB:AF:1A:A0:21:9F:6D:0C:AD' + } + ], + role : 'auto' + }; +}; + +exports.generateTransportRemoteParameters = function() +{ + return { + id : uuidv4(), + iceParameters : + { + iceLite : true, + password : 'yku5ej8nvfaor28lvtrabcx0wkrpkztz', + usernameFragment : 'h3hk1iz6qqlnqlne' + }, + iceCandidates : + [ + { + family : 'ipv4', + foundation : 'udpcandidate', + ip : '9.9.9.9', + port : 40533, + priority : 1078862079, + protocol : 'udp', + type : 'host' + }, + { + family : 'ipv6', + foundation : 'udpcandidate', + ip : '9:9:9:9:9:9', + port : 41333, + priority : 1078862089, + protocol : 'udp', + type : 'host' + } + ], + dtlsParameters : + { + fingerprints : + [ + { + algorithm : 'sha-256', + value : 'A9:F4:E0:D2:74:D3:0F:D9:CA:A5:2F:9F:7F:47:FA:F0:C4:72:DD:73:49:D0:3B:14:90:20:51:30:1B:90:8E:71' + }, + { + algorithm : 'sha-384', + value : '03:D9:0B:87:13:98:F6:6D:BC:FC:92:2E:39:D4:E1:97:32:61:30:56:84:70:81:6E:D1:82:97:EA:D9:C1:21:0F:6B:C5:E7:7F:E1:97:0C:17:97:6E:CF:B3:EF:2E:74:B0' + }, + { + algorithm : 'sha-512', + value : '84:27:A4:28:A4:73:AF:43:02:2A:44:68:FF:2F:29:5C:3B:11:9A:60:F4:A8:F0:F5:AC:A0:E3:49:3E:B1:34:53:A9:85:CE:51:9B:ED:87:5E:B8:F4:8E:3D:FA:20:51:B8:96:EE:DA:56:DC:2F:5C:62:79:15:23:E0:21:82:2B:2C' + } + ], + role : 'auto' + }, + sctpParameters : + { + port : 5000, + numStreams : 2048, + maxMessageSize : 2000000 + } + }; +}; + +exports.generateProducerRemoteParameters = function() +{ + return { + id : uuidv4() + }; +}; + +exports.generateConsumerRemoteParameters = function({ id, codecMimeType } = {}) +{ + switch (codecMimeType) + { + case 'audio/opus': + { + return { + id : id || uuidv4(), + producerId : uuidv4(), + kind : 'audio', + rtpParameters : + { + codecs : + [ + { + mimeType : 'audio/opus', + payloadType : 100, + clockRate : 48000, + channels : 2, + rtcpFeedback : + [ + { type: 'transport-cc' } + ], + parameters : + { + useinbandfec : 1, + foo : 'bar' + } + } + ], + encodings : + [ + { + ssrc : 46687003 + } + ], + headerExtensions : + [ + { + uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', + id : 1 + }, + { + uri : 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + id : 5 + }, + { + uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + id : 10 + } + ], + rtcp : + { + cname : 'wB4Ql4lrsxYLjzuN', + reducedSize : true, + mux : true + } + } + }; + } + + case 'audio/ISAC': + { + return { + id : id || uuidv4(), + producerId : uuidv4(), + kind : 'audio', + rtpParameters : + { + codecs : + [ + { + mimeType : 'audio/ISAC', + payloadType : 111, + clockRate : 16000, + channels : 1, + rtcpFeedback : + [ + { type: 'transport-cc' } + ], + parameters : {} + } + ], + encodings : + [ + { + ssrc : 46687004 + } + ], + headerExtensions : + [ + { + uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', + id : 1 + }, + { + uri : 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + id : 5 + } + ], + rtcp : + { + cname : 'wB4Ql4lrsxYLjzuN', + reducedSize : true, + mux : true + } + } + }; + } + + case 'video/VP8': + { + return { + id : id || uuidv4(), + producerId : uuidv4(), + kind : 'video', + rtpParameters : + { + codecs : + [ + { + mimeType : 'video/VP8', + payloadType : 101, + clockRate : 90000, + rtcpFeedback : + [ + { type: 'nack' }, + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'goog-remb' }, + { type: 'transport-cc' } + ], + parameters : + { + 'x-google-start-bitrate' : 1500 + } + }, + { + mimeType : 'video/rtx', + payloadType : 102, + clockRate : 90000, + rtcpFeedback : [], + parameters : + { + apt : 101 + } + } + ], + encodings : + [ + { + ssrc : 99991111, + rtx : + { + ssrc : 99991112 + } + } + ], + headerExtensions : + [ + { + uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', + id : 1 + }, + { + uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + id : 4 + }, + { + uri : 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + id : 5 + }, + { + uri : 'urn:3gpp:video-orientation', + id : 11 + }, + { + uri : 'urn:ietf:params:rtp-hdrext:toffset', + id : 12 + } + ], + rtcp : + { + cname : 'wB4Ql4lrsxYLjzuN', + reducedSize : true, + mux : true + } + } + }; + } + + default: + { + throw new TypeError(`unknown codecMimeType "${codecMimeType}"`); + } + } +}; + +exports.generateDataProducerRemoteParameters = function() +{ + return { + id : uuidv4() + }; +}; + +exports.generateDataConsumerRemoteParameters = function({ id } = {}) +{ + return { + id : id || uuidv4(), + dataProducerId : uuidv4(), + sctpStreamParameters : + { + streamId : 666, + maxPacketLifeTime : 5000, + maxRetransmits : undefined + } + }; +}; diff --git a/meet/server/test/test.js b/meet/server/test/test.js new file mode 100644 index 00000000..d8de75b8 --- /dev/null +++ b/meet/server/test/test.js @@ -0,0 +1,159 @@ +const assert = require('assert'); +let request = require('supertest') +const io = require("socket.io-client"); + +// const mediasoupClient = require('mediasoup-client'); +// const { FakeHandler } = require('../node_modules/mediasoup-client/lib/handlers/FakeHandler'); +const fakeParameters = require('./fakeParameters'); + +let app + +before(function (done) { + process.env.SSL_CERT = "../../docker/certs/kolab.hosted.com.cert" + process.env.SSL_KEY = "../../docker/certs/kolab.hosted.com.key" + process.env.REDIS_IP = "none" + // process.env.DEBUG = '*' + app = require('../server.js') + request = request(app); + + app.on("ready", function(){ + done(); + }); +}); + +describe('GET /ping', function() { + it('responds', function(done) { + request + .get('/meetmedia/api/ping') + .expect(200, done); + }); +}); + +describe('Join room', function() { + const roomId = "room1"; + let signalingSocket + let peerId + + async function sendRequest(socket, method, data = null) { + return await new Promise((resolve, /*reject*/) => { + socket.emit( + 'request', + {method: method, + data: data}, + (error, response) => { + assert(!error) + resolve(response) + } + ) + }) + } + + it('create room', function(done) { + request + .post(`/meetmedia/api/sessions/${roomId}/connection`) + .expect(200) + .then((res) => { + let data = res.body; + peerId = data['id']; + const signalingUrl = data['token']; + assert(signalingUrl.includes(peerId)) + assert(signalingUrl.includes(roomId)) + console.info(signalingUrl); + + signalingSocket = io(signalingUrl, { path: '/meetmedia/signaling', transports: ["websocket"], rejectUnauthorized: false }); + signalingSocket.on('notification', (reason) => + { + console.warn('Received notification "%s"', reason); + if (reason['method'] == 'roomReady') { + done(); + } + }); + + signalingSocket.connect(); + }) + .catch(err => { console.warn(err); done(err)}) + }); + + it('getRtpCapabilities', async () => { + const routerRtpCapabilities = await sendRequest(signalingSocket, 'getRouterRtpCapabilities') + assert(Object.keys(routerRtpCapabilities).length != 0) + }); + + + it('join', async () => { + const { id, role, peers } = await sendRequest(signalingSocket, 'join', { + nickname: "nickname", + rtpCapabilities: fakeParameters.generateNativeRtpCapabilities() + }) + assert.equal(id, peerId) + assert.equal(role, 0) + assert.equal(peers.length, 0) + }) + + it('second peer joining', function(done) { + request + .post(`/meetmedia/api/sessions/${roomId}/connection`) + .expect(200) + .then((res) => { + let data = res.body; + const newId = data['id']; + const signalingUrl = data['token']; + + let signalingSocket2 = io(signalingUrl, { path: '/meetmedia/signaling', transports: ["websocket"], rejectUnauthorized: false }); + signalingSocket2.on('notification', async (reason) => + { + console.warn('Received peer2 notification "%s"', reason); + if (reason['method'] == 'roomReady') { + const { peers } = await sendRequest(signalingSocket2, 'join', { + nickname: "nickname", + rtpCapabilities: fakeParameters.generateNativeRtpCapabilities() + }) + assert.equals(peers.length, 1) + assert.equals(peers[0].id, peerId) + } + }); + + signalingSocket.on('notification', (reason) => + { + console.warn('Received peer1 notification "%s"', reason); + if (reason.method == 'newPeer') { + assert(reason.data.id == newId); + done(); + } + }); + + signalingSocket.connect(); + }) + .catch(err => { console.warn(err); done(err)}) + }); + + // it('createDevice', async () => { + // let device; + // try{ + // device = new mediasoupClient.Device({ handlerFactory: FakeHandler.createFactory(fakeParameters) }); + // } catch (error) { + // console.warn(error) + // if (error.name === 'UnsupportedError') { + // console.warn('browser not supported'); + // } + // } + // // try { + // // await device.load(routerRtpCapabilities) + // // } catch (err) { + // // console.warn("Device loading failed", err); + // // } + // // assert(device.canProduce('video')) + // // console.warn("So can we produce?", device.canProduce('video')) + // // return true; + // }); + + after(function () { + signalingSocket.close(); + }) + +}); + +after(function () { + process.exit(); +}) +