460 lines
13 KiB
JavaScript
460 lines
13 KiB
JavaScript
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);
|
|
// }
|
|
//}
|