diff --git a/app/gulpfile.js b/app/gulpfile.js index 3eaa076..a214167 100644 --- a/app/gulpfile.js +++ b/app/gulpfile.js @@ -206,6 +206,7 @@ gulp.task('livebrowser', (done) => { open : 'external', host : config.domain, + port : 3000, server : { baseDir : OUTPUT_DIR @@ -226,6 +227,7 @@ gulp.task('browser', (done) => { open : 'external', host : config.domain, + port : 3000, server : { baseDir : OUTPUT_DIR diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 464c2c2..a3ff903 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -20,9 +20,9 @@ const ROOM_OPTIONS = const VIDEO_CONSTRAINS = { - qvga : { width: { ideal: 320 }, height: { ideal: 240 } }, - vga : { width: { ideal: 640 }, height: { ideal: 480 } }, - hd : { width: { ideal: 1280 }, height: { ideal: 720 } } + qvga : { width: { ideal: 320 }, height: { ideal: 240 }, aspectRatio: 1.334 }, + vga : { width: { ideal: 640 }, height: { ideal: 480 }, aspectRatio: 1.334 }, + hd : { width: { ideal: 800 }, height: { ideal: 600 }, aspectRatio: 1.334 } }; export default class RoomClient @@ -37,6 +37,9 @@ export default class RoomClient const protooUrl = getProtooUrl(peerName, roomId); const protooTransport = new protooClient.WebSocketTransport(protooUrl); + // window element to external login site + this._loginWindow; + // Closed flag. this._closed = false; @@ -60,6 +63,7 @@ export default class RoomClient // mediasoup-client Room instance. this._room = new mediasoupClient.Room(ROOM_OPTIONS); + this._room.roomId = roomId; // Transport for sending. this._sendTransport = null; @@ -111,6 +115,18 @@ export default class RoomClient this._dispatch(stateActions.setRoomState('closed')); } + login() + { + const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`; + + this._loginWindow = window.open(url, 'loginWindow'); + } + + closeLoginWindow() + { + this._loginWindow.close(); + } + changeDisplayName(displayName) { logger.debug('changeDisplayName() [displayName:"%s"]', displayName); @@ -750,6 +766,35 @@ export default class RoomClient break; } + + // This means: server wants to change MY displayName + case 'auth': + { + logger.debug('got auth event from server', request.data); + accept(); + + if (request.data.verified == true) + { + this.changeDisplayName(request.data.name); + this._dispatch(requestActions.notify( + { + text : `Authenticated successfully: ${request.data}` + } + )); + } + else + { + this._dispatch(requestActions.notify( + { + text : `Authentication failed: ${request.data}` + } + )); + } + this.closeLoginWindow(); + break; + + } + case 'raisehand-message': { accept(); diff --git a/app/lib/components/Peers.jsx b/app/lib/components/Peers.jsx index fdd4a92..abb8d62 100644 --- a/app/lib/components/Peers.jsx +++ b/app/lib/components/Peers.jsx @@ -13,24 +13,23 @@ class Peers extends React.Component { super(); this.state = { - ratio : 4 / 3 + ratio : 1.334 }; - } updateDimensions() { const n = this.props.peers.length; - if (n == 0) + if (n == 0) { return; } const width = this.refs.peers.clientWidth; const height = this.refs.peers.clientHeight; - + let x, y, space; - + for (let rows = 1; rows < 100; rows = rows + 1) { x = width / Math.ceil(n / rows); @@ -43,8 +42,8 @@ class Peers extends React.Component } space = height - (y * (rows)); if (space < y) - { - break; + { + break; } } if (Math.ceil(this.props.peerWidth) !== Math.ceil(0.9 * x)) @@ -52,17 +51,17 @@ class Peers extends React.Component this.props.onComponentResize(0.9 * x, 0.9 * y); } } - + componentDidMount() { window.addEventListener('resize', this.updateDimensions.bind(this)); } - - componentWillUnmount() + + componentWillUnmount() { window.removeEventListener('resize', this.updateDimensions.bind(this)); } - + render() { const { @@ -71,15 +70,15 @@ class Peers extends React.Component peerWidth, peerHeight } = this.props; - - const style = + + const style = { 'width' : peerWidth, 'height' : peerHeight }; this.updateDimensions(); - + return (
{ @@ -127,7 +126,7 @@ const mapStateToProps = (state) => // TODO: This is not OK since it's creating a new array every time, so triggering a // component rendering. const peersArray = Object.values(state.peers); - + return { peers : peersArray, activeSpeakerName : state.room.activeSpeakerName, diff --git a/app/stylus/components/PeerView.styl b/app/stylus/components/PeerView.styl index 1230284..f5dbdc0 100644 --- a/app/stylus/components/PeerView.styl +++ b/app/stylus/components/PeerView.styl @@ -170,7 +170,7 @@ flex: 100 100 auto; height: 100%; width: 100%; - object-fit: cover; + object-fit: contain; user-select: none; transition-property: opacity; transition-duration: .15s; diff --git a/server/config.example.js b/server/config.example.js index 43695cb..8ad774b 100644 --- a/server/config.example.js +++ b/server/config.example.js @@ -1,5 +1,18 @@ module.exports = { + // oAuth2 conf + oauth2 : + { + client_id : '', + client_secret : '', + providerID : '', + redirect_uri : 'https://mYDomainName:port/auth-callback', + authorization_endpoint : '', + userinfo_endpoint : '', + token_endpoint : '', + scopes : { request : [ 'openid', 'userid','profile'] }, + response_type : 'code' + }, // Listening hostname for `gulp live|open`. domain : 'localhost', tls : @@ -7,6 +20,8 @@ module.exports = cert : `${__dirname}/certs/mediasoup-demo.localhost.cert.pem`, key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem` }, + // Listening port for https server. + listeningPort : 3443, mediasoup : { // mediasoup Server settings. diff --git a/server/http-helpers.js b/server/http-helpers.js new file mode 100644 index 0000000..de66801 --- /dev/null +++ b/server/http-helpers.js @@ -0,0 +1,31 @@ +'use strict'; + +const headers = { + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", + "access-control-allow-headers": "content-type, accept", + "access-control-max-age": 10, + "Content-Type": "application/json" +}; + +exports.prepareResponse = function(req, cb) { + var data = ""; + req.on('data', function(chunk) { data += chunk; }); + req.on('end', function() { cb(data); }); +}; + +exports.respond = function(res, data, status) { + status = status || 200; + res.writeHead(status, headers); + res.end(data); +}; + +exports.send404 = function(res) { + exports.respond(res, 'Not Found', 404); +}; + +exports.redirector = function(res, loc, status) { + status = status || 302; + res.writeHead(status, { Location: loc }); + res.end(); +}; diff --git a/server/lib/Room.js b/server/lib/Room.js index dbe067e..c6b5089 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -268,7 +268,6 @@ class Room extends EventEmitter const { mediaPeer } = protooPeer.data; mediaPeer.appData.raiseHand = request.data.raiseHandState; - // Spread to others via protoo. this._protooRoom.spread( 'raisehand-message', diff --git a/server/router.js b/server/router.js new file mode 100644 index 0000000..97da297 --- /dev/null +++ b/server/router.js @@ -0,0 +1,194 @@ +'use strict'; + +const EventEmitter = require( 'events' ); +const eventEmitter = new EventEmitter(); +const path = require('path'); +const url = require('url'); +const httpHelpers = require('./http-helpers'); +const fs = require('fs'); +const config = require('./config'); +const utils = require('./util'); +const querystring = require('querystring'); +const https = require('https') +const Logger = require('./lib/Logger'); + +const logger = new Logger(); + +let authRequests = {}; // ongoing auth requests : +/* +{ + state: + { + peerName:'peerName' + code:'oauth2 code', + roomId: 'romid', + } +} +*/ +const actions = { + 'GET': function(req, res) { + var parsedUrl = url.parse(req.url,true); + if ( parsedUrl.pathname === '/auth-callback' ) + { + if ( typeof(authRequests[parsedUrl.query.state]) != 'undefined' ) + { + console.log('got authorization code for access token: ',parsedUrl.query,authRequests[parsedUrl.query.state]); + const auth = "Basic " + new Buffer(config.oauth2.client_id + ":" + config.oauth2.client_secret).toString("base64"); + const postUrl = url.parse(config.oauth2.token_endpoint); + let postData = querystring.stringify({ + "grant_type":"authorization_code", + "code":parsedUrl.query.code, + "redirect_uri":config.oauth2.redirect_uri + }); + + let request = https.request( { + host : postUrl.hostname, + path : postUrl.pathname, + port : postUrl.port, + method : 'POST', + headers : + { + 'Content-Type' : 'application/x-www-form-urlencoded', + 'Authorization' : auth, + 'Content-Length': Buffer.byteLength(postData) + } + }, function(res) + { + res.setEncoding("utf8"); + let body = ""; + res.on("data", data => { + body += data; + }); + res.on("end", () => { + if ( res.statusCode == 200 ) + { + console.log('We\'ve got an access token!', body); + body = JSON.parse(body); + authRequests[parsedUrl.query.state].access_token = + body.access_token; + const auth = "Bearer " + body.access_token; + const getUrl = url.parse(config.oauth2.userinfo_endpoint); + let request = https.request( { + host : getUrl.hostname, + path : getUrl.pathname, + port : getUrl.port, + method : 'GET', + headers : + { + 'Authorization' : auth, + } + }, function(res) + { + res.setEncoding("utf8"); + let body = ''; + res.on("data", data => { + body += data; + }); + res.on("end", () => { + // we don't need this any longer: + delete authRequests[parsedUrl.query.state].access_token; + + body = JSON.parse(body); + console.log(body); + if ( res.statusCode == 200 ) + { + authRequests[parsedUrl.query.state].verified = true; + if ( typeof(body.sub) != 'undefined') + { + authRequests[parsedUrl.query.state].sub = body.sub; + } + if ( typeof(body.name) != 'undefined') + { + authRequests[parsedUrl.query.state].name = body.name; + } + if ( typeof(body.picture) != 'undefined') + { + authRequests[parsedUrl.query.state].picture = body.picture; + } + } else { + { + authRequests[parsedUrl.query.state].verified = false; + } + } + eventEmitter.emit('auth', + authRequests[parsedUrl.query.state]); + + delete authRequests[parsedUrl.query.state]; + }); + }); + request.write(' '); + request.end; + } + else + { + console.log('access_token denied',body); + authRequests[parsedUrl.query.state].verified = false; + delete authRequests[parsedUrl.query.state].access_token; + eventEmitter.emit('auth', + authRequests[parsedUrl.query.state]); + } + }); + }); + request.write(postData); + request.end; + } + else + { + logger.warn('Got authorization_code for unseen state:', parsedUrl) + } + } + else if (parsedUrl.pathname === '/login') { + const state = utils.random(10); + httpHelpers.redirector(res, config.oauth2.authorization_endpoint + + '?client_id=' + config.oauth2.client_id + + '&redirect_uri=' + config.oauth2.redirect_uri + + '&state=' + state + + '&scopes=' + config.oauth2.scopes.request.join('+') + + '&response_type=' + config.oauth2.response_type); + authRequests[state] = + { + 'roomId' : parsedUrl.query.roomId, + 'peerName' : parsedUrl.query.peerName + }; + console.log('Started authorization process: ', parsedUrl.query); + } + else + { + console.log('requested url:', parsedUrl.pathname); + var resolvedBase = path.resolve('./public'); + var safeSuffix = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, ''); + var fileLoc = path.join(resolvedBase, safeSuffix); + + var stream = fs.createReadStream(fileLoc); + + // Handle non-existent file -> delivering index.html + stream.on('error', function(error) { + stream = fs.createReadStream(path.resolve('./public/index.html')); + res.statusCode = 200; + stream.pipe(res); + }); + + // File exists, stream it to user + res.statusCode = 200; + stream.pipe(res); + } + }, + + 'POST': function(req, res) { + httpHelpers.prepareResponse(req, function(data) { + // Do something with the data that was just collected by the helper + // e.g., validate and save to db + // either redirect or respond + // should be based on result of the operation performed in response to the POST request intent + // e.g., if user wants to save, and save fails, throw error + httpHelpers.redirector(res, /* redirect path , optional status code - defaults to 302 */); + }); + } +}; + +module.exports = eventEmitter; + +module.exports.handleRequest = function(req, res) { + var action = actions[req.method]; + action ? action(req, res) : httpHelpers.send404(res); +}; diff --git a/server/server.js b/server/server.js index 07a9664..514b089 100755 --- a/server/server.js +++ b/server/server.js @@ -14,7 +14,9 @@ console.log('- config.mediasoup.logTags:', config.mediasoup.logTags); const fs = require('fs'); const https = require('https'); +const router = require('./router'); const url = require('url'); +const path = require('path'); const protooServer = require('protoo-server'); const mediasoup = require('mediasoup'); const readline = require('readline'); @@ -77,25 +79,34 @@ mediaServer.on('newroom', (room) => }); }); -// HTTPS server for the protoo WebSocket server. +// HTTPS server const tls = { cert : fs.readFileSync(config.tls.cert), key : fs.readFileSync(config.tls.key) }; -const httpsServer = https.createServer(tls, (req, res) => +const httpsServer = https.createServer(tls, router.handleRequest); +httpsServer.listen(config.listeningPort, '0.0.0.0', () => { - res.writeHead(404, 'Not Here'); - res.end(); + logger.info('Server running, port: ',config.listeningPort); }); -httpsServer.listen(3443, '0.0.0.0', () => -{ - logger.info('protoo WebSocket server running'); -}); +router.on('auth',function(event){ + console.log('router: Got an event: ',event) + if ( rooms.has(event.roomId) ) + { + const room = rooms.get(event.roomId)._protooRoom; + if ( room.hasPeer(event.peerName) ) + { + const peer = room.getPeer(event.peerName); + peer.send('auth', event) + } + } +}) -// Protoo WebSocket server. +// Protoo WebSocket server listens to same webserver so everythink is available +// via same port const webSocketServer = new protooServer.WebSocketServer(httpsServer, { maxReceivedFrameSize : 960000, // 960 KBytes. diff --git a/server/util.js b/server/util.js new file mode 100644 index 0000000..cfa4548 --- /dev/null +++ b/server/util.js @@ -0,0 +1,18 @@ +'use strict'; + +var crypto = require('crypto'); + +exports.random = function (howMany, chars) { + chars = chars + || "abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789"; + var rnd = crypto.randomBytes(howMany) + , value = new Array(howMany) + , len = len = Math.min(256, chars.length) + , d = 256 / len + + for (var i = 0; i < howMany; i++) { + value[i] = chars[Math.floor(rnd[i] / d)] + }; + + return value.join(''); +}