From b60df56157ee1fd0bd4938799bac05a62fda91a1 Mon Sep 17 00:00:00 2001 From: lookshe Date: Sat, 14 Mar 2015 20:45:20 +0100 Subject: initial commit from working version --- signaling-server/server.js | 460 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 signaling-server/server.js (limited to 'signaling-server/server.js') diff --git a/signaling-server/server.js b/signaling-server/server.js new file mode 100644 index 0000000..00f915b --- /dev/null +++ b/signaling-server/server.js @@ -0,0 +1,460 @@ +var http = require('http') + , https = require('https') + , sio = require('socket.io') + , fs = require('fs') + //, RedisStore = sio.RedisStore; + +// +// Load configuration file +var config = JSON.parse( + fs.readFileSync(__dirname + "/config.json").toString().replace( + new RegExp("\\/\\*(.|\\r|\\n)*?\\*\\/", "g"), + "" // strip out comments + ) +); + +// +// Create server instance +var server = (config.ssl && config.ssl.enabled ? + // HTTPS + https.createServer({ + key: fs.readFileSync(config.ssl.key) + , cert: fs.readFileSync(config.ssl.cert) + }) : + // HTTP + http.createServer()) + .listen(config.port, '127.0.0.1', function() { + var addr = server.address(); + console.log('Symple server listening on ' + (config.ssl && config.ssl.enabled ? 'https' : 'http') + '://' + addr.address + ':' + addr.port); + }); + + +var io = sio.listen(server); +io.set('log level', 1); + +// +// Socket.IO Configuration +io.configure(function () { + + if (config.redis) { + + // Initialize the redis store + var store = new sio.RedisStore({ + nodeID: config.nodeId || 1, + redisPub: config.redis || {}, + redisSub: config.redis || {}, + redisClient: config.redis || {} + }); + + // Authenticate redis connections if required + if (config.redis && config.redis.password) { + store.pub.auth(config.redis.password) + store.sub.auth(config.redis.password) + store.cmd.auth(config.redis.password) + } + + io.set('store', store); + } +}); + + +// +// Globals +// + +function isfunc(obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); +} + +function respond(ack, status, message, data) { + if (ack && isfunc(ack)) { + res = {} + res.type = 'response'; + res.status = status; + res.message = message; + if (data) + res.data = data.data ? data.data : data; + console.log('Responding: ', res); + ack(res); + } +} + +// Parses a Symple endpoint address with the following +// format: user@group/id +function parseAddress(str) { + var addr = {}, base, + arr = str.split("/") + + if (arr.length < 2) // no id + base = str; + else { // has id + addr.id = arr[1]; + base = arr[0]; + } + + arr = base.split("@") + if (arr.length < 2) // group only + addr.group = base; + else { // group and user + addr.user = arr[0]; + addr.group = arr[1]; + } + + return addr; +} + +function buildAddress(peer) { + return peer.user + "@" + peer.group + "/" + peer.id; +} + + +// +// Socket.IO Socket extensions +// + +sio.Socket.prototype.authorize = function(req, fn) { + + var client = this; + + // Authenticated Access + if (!config.anonymous) { + if (!req.user || !req.token) + return fn(400, 'Bad request'); + + // Retreive the session from Redis + client.token = req.token; // Remote session token + client.getSession(function(err, session) { + //console.log('Authenticating: ', req.token, ':', session); + if (err || typeof session !== 'object' || typeof session.user !== 'object') { + //console.log('Authentication error: ', req.token, ':', err); + return fn(401, 'Authentication failed'); + } + else { + //console.log('Authentication success: ', req); + client.session = session; // Remote session object + client.group = session.user.group; // The client's parent group + client.access = session.user.access; // The client access level [1 - 10] + client.user = session.user.user; // The client login name + client.user_id = session.user.user_id;// The client login user ID + client.onAuthorize(req); + return fn(200, 'Welcome ' + client.name); + } + }); + } + + // Anonymous Access + else { + if (!req.user) + return fn(400, 'Bad request'); + + client.access = -1; + client.name = req.name; + client.group = req.group; + client.user = req.user; + client.user_id = req.user_id; + client.onAuthorize(req); + return fn(200, 'Welcome ' + client.name); + } +} + + +sio.Socket.prototype.onAuthorize = function(req) { + console.log(this.id, 'on authorize: ', req); + this.online = true; + this.name = req.name ? // The client display name + req.name : this.user; + this.type = req.type; // The client type + this.join('user-' + this.user); // join user channel + this.join('group-' + this.group); // join group channel +} + + +sio.Socket.prototype.toPresence = function(p) { + if (!p || typeof p !== 'object') + p = {}; + p.type = 'presence'; + p.data = this.toPeer(p.data); + if (!p.from) + p.from = this.toAddress(); + //if (!p.from || typeof p.from !== 'object') { + // p.from = {}; + // p.from.name = this.name; + //} + return p; +} + + +sio.Socket.prototype.toPeer = function(p) { + if (!p || typeof p !== 'object') + p = {}; + p.id = this.id; //sympleID; + p.type = this.type; + p.node = this.node; + p.user = this.user; + p.user_id = this.user_id; + p.group = this.group; + p.access = this.access; + p.online = this.online; + p.host = this.handshake.headers['x-real-ip'] + || this.handshake.headers['x-forwarded-for'] + || this.handshake.address.address; //this.handshake ? : ''; + + // allow client to change name + if (typeof p.name === 'string') + this.name = p.name; + else + p.name = this.name; + + return p; +} + + +sio.Socket.prototype.toAddress = function() { + return this.user + "@" + this.group + "/" + this.id; +} + + +sio.Socket.prototype.getSessionKey = function(fn) { + // token must be set + io.store.cmd.keys("symple:*:" + this.token, function(err, keys) { + fn(err, keys.length ? keys[0] : null) + }); +} + + +sio.Socket.prototype.getSession = function(fn) { + this.getSessionKey(function(err, key) { + if (key) { + io.store.cmd.get(key, function(err, session) { + fn(err, JSON.parse(session)); + }); + } + else fn("No session", null); + }); +} + + +sio.Socket.prototype.touchSession = function(fn) { + this.getSessionKey(function(err, key) { + if (key) { + // expire in 15 mins + io.store.cmd.expire(key, 15 * 60, fn); + } + else fn("No session", null); + }); +} + +sio.Socket.prototype.getDestinationAddress = function(message) { + switch(typeof message.to) { + case 'object': + return message.to; + case 'string': + return parseAddress(message.to); + case 'undefined': + return { group: this.group }; + } +} + + +sio.Socket.prototype.broadcastMessage = function(message) { + if (!message || typeof message !== 'object' || !message.from) { + console.error(this.id, 'dropping invalid message:', message); + return; + } + + // Replace from address with server-side peer data for security. + //message.from.id = this.id; + //message.from.type = this.type; + //message.from.group = this.group; + //message.from.access = this.access; + //message.from.user = this.user; + //message.from.user_id = this.user_id; + + // Get an destination address object for routing + var to = this.getDestinationAddress(message); + + // Make sure we have a valid destination address + if (typeof to !== 'object' || typeof to.group === 'undefined') { + console.error(this.id, 'dropping invalid message without destination:', to, ':', message); + return; + } + + // If a session id was given we send a directed message to that session id. + if (typeof to.id === 'string' && to.id.length) { + this.namespace/*.except(this.unauthorizedIDs())*/.socket(to.id).json.send(message); + } + + // If a user was given (but no session id) we broadcast a message to user scope. + // TODO: Ensure group membership + else if (to.user && typeof to.user === 'string') { + this.broadcast.to('user-' + to.user/*, this.unauthorizedIDs()*/).json.send(message); + } + + // If a group was given (but no session id or user) we broadcast to group scope. + else if (to.group && typeof to.group === 'string') { + this.broadcast.to('group-' + to.group/*, this.unauthorizedIDs()*/).json.send(message); + } + + else { + console.error(this.id, 'cannot route invalid message:', message); + } +} + + +// +// Socket.IO connection handler +// + +io.sockets.on('connection', function(client) { + + // 5 seconds to Announce or get booted + var interval = setInterval(function () { + console.log(client.id, 'failed to announce'); + client.disconnect(); + }, 5000); + + // Announce + client.on('announce', function(req, ack) { + console.log(client.id, 'announcing:', req); + + try { + + // Authorization + client.authorize(req, function(status, message) { + // console.log(client.id, 'announce result:', status); + clearInterval(interval); + if (status == 200) + respond(ack, status, message, client.toPeer()); + else { + respond(ack, status, message); + client.disconnect(); + return; + } + + // Message + client.on('message', function(m, ack) { + if (m) { + if (m.type == 'presence') + this.toPresence(m); + client.broadcastMessage(m); + respond(ack, 200, 'Message received'); + } + }); + + // Peers + client.on('peers', function(ack) { + respond(ack, 200, '', this.peers(false)); + }); + + // Timer + if (config.redis) { + // Keep sessions from expiring while connected + interval = setInterval(function () { + // Touch the client session event 10 + // minutes to prevent it from expiring. + client.touchSession(function(err, res) { + console.log(client.id, 'touching session:', !!res); + }); + }, 10 * 60000); + } + + }); + } + catch (e) { + console.log(client.id, 'internal error: ', e); + client.disconnect(); + } + }); + + // + // Disconnection + client.on('disconnect', function() { + console.log(client.id, 'is disconnecting'); + clearInterval(interval); + if (client.online) { + client.online = false; + var p = client.toPresence(); + //console.log('Disconnecting', p); + client.broadcastMessage(p); + } + client.leave('user-' + client.user); // leave user channel + client.leave('group-' + client.group); // leave group channel + }); +}); + + +// +// Socket.IO Manager extensions +// + +//sio.Socket.prototype.authorizedClients = function() { +// var res = []; +// var clients = io.sockets.clients(this.group); +// for (i = 0; i < clients.length; i++) { +// if (clients[i].access >= this.access) +// res.push(clients[i]); +// } +// return res; +//} + + +// Returns an array of authorized peers belonging to the currect +// client socket group. +//sio.Socket.prototype.peers = function(includeSelf) { +// res = [] +// //var clients = this.authorizedClients(); +// var clients = io.sockets.clients('group-' + this.group); +// for (i = 0; i < clients.length; i++) { +// if ((!includeSelf && clients[i] == this) || +// clients[i].access > this.access) +// continue; +// res.push(clients[i].toPeer()); +// } +// return res; +//} + + +// Returns an array of group peer IDs that dont have permission +// to receive messages broadcast by the current peer ie. access +// is lower than the current peer. +//sio.Socket.prototype.unauthorizedIDs = function() { +// var res = []; +// var clients = io.sockets.clients('group-' + this.group); +// for (i = 0; i < clients.length; i++) { +// if (clients[i].access < this.access) +// res.push(clients[i].id); +// } +// console.log('Unauthorized IDs:', this.name, ':', this.access, ':', res); +// return res; +//} + +//function packetSender(packet) { +// var res = packet.match(/\"from\"[ :]+[ {]+[^}]*\"id\"[ :]+\"(.*?)\"/); +// return res ? io.sockets.sockets[res[1]] : null; +//} + +//onDispatchOriginal = sio.Manager.prototype.onDispatch; +//sio.Manager.prototype.onDispatch = function(room, packet, volatile, exceptions) { +// +// // Authorise outgoing messages via the onDispatch method so unprotected +// // data can not be published directly from Redis. +// var sender = packetSender(packet); +// if (sender) { +// if (!exceptions) +// exceptions = [sender.id]; // dont send to self +// exceptions = exceptions.concat(sender.unauthorizedIDs()); +// //console.log("Sending a message excluding: ", exceptions, ': ', sender.unauthorizedIDs()); +// onDispatchOriginal.call(this, room, packet, volatile, exceptions) +// } +//} + +//onClientDispatchOriginal = sio.Manager.prototype.onClientDispatch; +//sio.Manager.prototype.onClientDispatch = function (id, packet) { +// +// // Ensure the recipient has sufficient permission to recieve the message +// var sender = packetSender(packet); +// var recipient = io.sockets.sockets[id]; +// if (sender && recipient && recipient.access >= sender) { +// onClientDispatchOriginal.call(this, id, packet); +// } +//} -- cgit v1.2.3