node-token-sockjs
Version:
A wrapper around express, sockjs-node, and redis that provides token based authentication, a websocket based rpc-like abstraction, and an optional publish subscribe interface.
168 lines (155 loc) • 6.39 kB
JavaScript
var async = require("async"),
_ = require("lodash"),
uuid = require("uuid"),
utils = require("./utils"),
ALL_CHANNEL = "___all___",
SYNC_CHANNEL = "___sync___",
UUID_REGEXP = /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/;
module.exports = function(options){
var self = this;
if(options.routes)
self._routes = utils.expressRoutesToSocketActions(options.routes);
var requestToken = function(req, res){
async.waterfall([
function(callback){
self._debug("Sockjs token request received", req.params);
if(typeof self.authentication === "function")
self.authentication(req, callback); // use custom function with "req" as an argument
else if(typeof self.authentication === "string")
callback(null, req.session[self.authentication] ? true : false); // use custom session property
else
callback(null, req.session && req.session.auth); // default
}
], function(error, auth){
var type = utils.requestType(req);
if(error){
self._debug("Error issuing token", error);
res.status(500)[type]({ error: error.message || error });
}else if(!auth){
self._debug("Rejected token request");
res.status(403)[type]({ error: "Forbidden" });
}else{
var save = typeof auth === "object" ? auth : _.merge({ __host: req.hostname || req.host, __ip: req.ip, __ips: req.ips }, (req.session || {}));
utils.issueToken(self.redisClient, save, function(error, token){
self._debug("Attaching token to authentication data", token, save);
if(error)
res.status(500)[type]({ error: error.message || error });
else
res.status(200)[type]({ token: token });
});
}
});
};
if(options.customMiddleware && typeof options.customMiddleware === "function"){
self._debug("Creating token route and attaching custom middleware");
self.app.get(self.tokenRoute, options.customMiddleware, requestToken);
}else{
self._debug("Creating token route without custom middleware");
self.app.get(self.tokenRoute, requestToken);
}
if(options.ping){
self.socketController._ping = function(auth, data, callback, socket){
self._debug("Socket sent ping", socket.sid);
callback(null, { message: "pong" });
};
}
if(self.pubsubClient){
self._debug("Setting up publish-subscribe functionality");
self.pubsubClient.subscribe(ALL_CHANNEL);
self.pubsubClient.subscribe(SYNC_CHANNEL);
self.pubsubClient.on("message", function(channel, message){
if(channel === SYNC_CHANNEL){
utils.handleInternal(self, message);
}else if(channel === ALL_CHANNEL){
self._debug("Broadcasting message to all sockets on all channels", message);
async.each(Object.keys(self._channels), function(_channel, callback){
utils.writeSockets(self._channels[_channel], _channel, message, self._filter);
callback();
});
}else{
self._debug("Publish-subscribe message received", channel, message);
if(self._channels[channel])
utils.writeSockets(self._channels[channel], channel, message, self._filter);
}
});
}
self.socketServer.on("connection", function(socket){
var sid = uuid.v4();
self._debug("Socket connection initiated", sid);
socket.sid = sid;
socket.created = new Date().getTime();
socket.channels = {};
self._sockets[sid] = socket;
socket.on("close", function(){
self._debug("Socket connection closed", sid);
if(self.pubsubClient && socket.channels){
_.each(socket.channels, function(bool, channel){
if(self._channels[channel]){
delete self._channels[channel][socket.sid];
if(Object.keys(self._channels[channel]).length === 0){
self.pubsubClient.unsubscribe(channel);
delete self._channels[channel];
}
}
});
}
delete self._sockets[sid];
});
socket.on("data", function(message){
try{
message = JSON.parse(message);
}catch(e){
return socket.write(JSON.stringify({ error: "Invalid message" }));
}
self._debug("Socket message received", message);
var sendMessage = function(error, resp){
if(error)
message.error = utils.serializeError(error);
else
message.resp = resp;
delete message.req;
self._debug("Writing message on socket", socket.sid, message);
socket.write(JSON.stringify(message));
};
if(message.rpc === "auth" && message.token){
self._debug("Socket auth request received", socket.sid, message.token);
if(!message.token.match(UUID_REGEXP)){
delete self._sockets[socket.sid];
return socket.end();
}
utils.verifyToken(self.redisClient, message.token, function(error, data){
if(error || !data){
self._debug("Error verifying token", socket.sid, message.token, error);
sendMessage(error.message || error || "Invalid token");
delete self._sockets[socket.sid];
socket.end();
}else{
self._debug("Successfully verified token", message.token, data);
socket.auth = data;
socket.host = data.__host;
socket.ip = data.__ip;
socket.ips = data.__ips;
delete socket.auth.__ip;
delete socket.auth.__ips;
delete socket.auth.__host;
utils.revokeToken(self.redisClient, message.token, function(err){
if(err)
self._debug("Error revoking token!", socket.sid, message.token, err);
});
utils.checkListeners(self._emitter, "authentication", socket, socket.auth, function(){
sendMessage(null, "success");
});
}
});
}else if(!socket.auth){
self._debug("Socket attempting to perform action before authorizing. Ending connection.", socket.sid, message);
message.error = "Forbidden";
socket.write(JSON.stringify(message));
socket.end();
delete self._sockets[socket.sid];
}else{
self._rpc(socket, message, sendMessage);
}
});
});
};