How to share dynamic objects across workers? - node.js

I'm trying to make a game, which works on rooms, lobby and such (imagine the chat app, except with additional checks/information storing).
Let's say, I have a module room.js
var EventEmitter = require('events');
class Room extends EventEmitter {
constructor (id, name) {
super();
this.id = id;
this.name = name;
this.users = [];
}
}
Room.prototype.addUser = function (user) {
if(this.users.indexOf(user) === -1) {
this.users.push(user);
this.emit('user_joined', user);
} else {
/* error handling */
}
};
module.exports = {
Room: Room,
byId: function (id) {
// where should I look up?
}
};
How can I get exactly this object (with events)? How can I access events emitted by this object?
In a single instance of node, I would do something like:
var rooms = [];
var room = new Room(1234, 'test room');
room.on('user_joined', console.log);
rooms.push(room);
Also, I don't quite understood how Redis is actually helping (is it replacement of EventEmitter?)
Regards.
EDIT: Would accept PM2 solutions too.

Instead of handling rooms in Node, you can replace them with channels in Redis).
When a new client wants to join in a room, the NodeJS app returns it the ID of this given room (that is to say the name of the channel), then the client suscribes to the selected room (your client is directly connected to Redis.
You can use a Redis Set to manage the list of rooms.
In this scenario, you don't need any event emitter, and your node servers are stateless.
Otherwise, it would mean Redis would be exposed on the Internet (assuming your game is public), so you must activate Redis authentication. A major problem with this solution is that you have to give the server password to all clients, so it's definitely unsecure.
Moreover, Redis' performances allow brute force attacks so exposing it on Internet is not recommended. That's why I think all communications should go through a Node instance, even if Redis is used as a backend.
To solve this, you can use socket.io to open sockets between Node and your clients, and make the Node instances (not the client) subscribe to the Redis channel. When a message is published by Redis, send it to the client through the socket. And add a layer of authentication to ensure only valid clients connect to a given channel.
Event emitter is not required. It's the Redis client which will be an event emitter (like in this example based on ioRedis)

Related

How to secure Redis & Socket.IO real-time server so only authenticated users can listen?

I'm building a web app that has a chat feature. I'm using Laravel 5.4 for the backend and Angular 4 for the front-end.
Everything is working (meaning I can broadcast and recieve) but I'm not at all sure how to secure it. The chat will always be 1 to 1 so its private and has to be secure. Each chat room will have a unique id but somebody could still listen in.
Currently I'm using JWTs for authentication when I make requests from my frontend to my API, but I'm not sure if its possible to implement something similar for this. I know I can pass the token from the frontend using the query option but than I'm unsure how to parse it and I'm also unsure how to verify that it indeed belongs to the user that is trying to access the chat (should I make a request to the API to verify in server.js? That doesn't seem efficient. Is it good enough to compare the user id of the token to the user id that will be passed in the data?)
If anybody has any advice or knows a better way to do it, it would be greatly apperciated
Event that is fired off from Laravel when a new message is posted
class NewMessage implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $data;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct()
{
$this->data = array(
'message'=> 'hi'
);
}
/**
* Get the channels the event should broadcast on.
*
* #return Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('chat');
}
}
server.js (node.js)
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var Redis = require('ioredis');
var redis = new Redis();
redis.psubscribe('private-chat', function(err, count) {
console.log('psubscribe');
});
redis.on('pmessage', function(subscribed, channel, message) {
console.log('pmessage', subscribed, channel, message);
message = JSON.parse(message);
io.emit(channel + ':' + message.event, message.data);
});
http.listen(3000, function(){
console.log('Listening on Port 3000');
});
Frontend component
socket: io.Socket;
this.socket = io.connect("http://webapp.test:3000", { query: this.token });
this.socket.on("private-chat:App\\Events\\NewMessage", (data) =>
{
console.log("Data", data);
});
Try to use SSL (confidentiality)
SSL as we know ensures that the server you're communicating with is actually the webservice you want.
Authentication (To avoid certificate pinning)
You need authentication in your case you are using JWT with HS256, I would strongly recommend you to use RS256 algorythm JWT in this case with private and public keys.
It doesn't matter that nobody else can listen to the conversation between the two of you if you don't know for sure who's on the other end (authentication).
Since I would build the app in the way that if session authentication between two channels its not set then never unfold the content of the data that is being send.
Unique session
As slong as your sevice is authenticated using JWT you send them back an authentication token. This is as simple as a random number or a GUID. This identifier will be required as part of any request to send or receive data on this channel during this session, it will only be accepted on this particular channel, and only as long as this unique session is open.
One more security check: link reply attack
I gave you the tips that I use everyday to seccurely send data back and forth with central banks (so following those tips I think your app should be pretty secure)
UPDATE
How to handle JWT
Create a new middleware Verify JWT token since the middleware in laravel its outter layer of core vendor that means if authentication fails it fails in outter layer not in core.
Group the routes under Verify JWT token middleware!
Create a RedisRepository this could be under App\RedisRepository. This class should be responsible for fetching data from redis.
On middleware decrypt user JWT get the decrypted payload(this might be user UUID or ID).
Fetch user ID from RedisRepository compare with the decrypted payload if positive authentication passes otherwise abort 403 unauthenticated!

Multiplayer game server with multiple node

Say you're making a Counter-Strike server. We have a single lobby and multiple rooms in which game sessions occur.
I.E I have 3 node server, nginx load balancer, redis.
SCHEMA
I'm using socket.io, with redis adapter, So I can emit messages to every clients on any server.
I can't understand one thing. For example I have something like this:
var currentGames = {};
socket.on('createGame', function(gameName){
var game = new Game();
game.addPlayer(socket.id);
currentGames.push();
// ... Send to all clients, that game created and they can join
});
socket.on('joinGame', function(gameName){
currentGames[gameName].addPlayer(socket.id);
// ... Send to all players of this game, that player joined
});
socket.on('walk', function(gameName, coordinates){
currentGames[socket.id].walk(player, coordinates);
// ... Get all players positions and send to clients
});
class Game
{
gameName;
players = {};
score;
constructor(name){
this.gameName = name;
}
addPlayer(player){
players[name] = new Player(player);
}
walk(id, coordinates){
this.players[id].updatePosition(coordinates);
}
}
class player
{
id;
life = 100;
coordinates = {0,0,0};
kills;
death;
constructor(id){
this.id = id;
}
updatePosition(coords){
this.coordinates = coords;
}
}
I've just written this code, for example.
let, user_1 is on node-1 and user-2 is on node-2.
If user_1 creates game, instance will be created and saved on node-1.
so when user_2 receives information about game, which was created and he clicks a join button,
client will send request to server and on node-2 there won't be the game instance, which user_1 created.
I hope, you will understand, what I'm talking (my English isn't good).
I guess, that currentGames object of game instances must be a global, for all nodes. But I don't know how.
Should I use redis or mongo or something else, for storing game information, instead of variable?
Am I going with wrong way?
U should sync Game State in Redis, room and game instance, too.
I have some note :
+ Entity State - manage Game State, store and set, get Game information to Redis
+ Client State - each connection client have Client State and Sync with Entity State, each tick in Game loop, u check difference beetween Entity State and Client State and send it to Client and update Game Client

how can I make private chat rooms with sockjs?

I am trying to make a chat system where only two users are able to talk to each other at a time ( much like facebook's chat )
I've tried multiplexing, using mongoDB's _id as the name so every channel is unique.
The problem I'm facing is that I cannot direct a message to a single client connection.
this is the client side code that first sends the message
$scope.sendMessage = function() {
specificChannel.send(message)
$scope.messageText = '';
};
this is the server side receiving the message
specificChannel.on('connection', function (conn) {
conn.on('data', function(message){
conn.write('message')
}
}
When I send a message, to any channel, every channel still receives the message.
How can I make it so that each client only listens to the messages sent to a specific channel?
It appeared that SockJS doesn't support "private" channels. I used the following solution for a similar issue:
var channel_id = 'my-very-private-channel'
var connection = new SockJS('/pubsub', '')
connection.onopen = function(){
connection.send({'method': 'set-channel', 'data': {'channel': channel_id}})
}
Backend solution is specific for every technology stack so I can't give a universal solution here. General idea is the following:
1) Parse the message in "on_message" function to find the requested "method name"
2) If the method is "set-channel" -> set the "self.channel" to this value
3) Broadcast further messages to subscribers with the same channel (I'm using Redis for that, but it also depends on your platform)
Hope it helps!

Pusher binding to events regardless of channel

I am attempting to listen to a particular event type regardless of the channel it was triggered in. My understanding of the docs (http://pusher.com/docs/client_api_guide/client_events#bind-events/lang=js) was that I can do so by calling the bind method on the pusher instance rather than on a channel instance. Here is my code:
var pusher = new Pusher('MYSECRETAPPKEY', {'encrypted':true}); // Replace with your app key
var eventName = 'new-comment';
var callback = function(data) {
// add comment into page
console.log(data);
};
pusher.bind(eventName, callback);
I then used the Event Creator tool in my account portal to generate an event. I used a random channel name, set the Event to "new-comment" and just put in some random piece of text into the Event Data. But, I am getting nothing appearing in my Console.
I am using https://d3dy5gmtp8yhk7.cloudfront.net/2.1/pusher.min.js, and performing this test in the latest Chrome.
What am I missing?
Thanks!
Shaheeb R.
Pusher will only send events to the client if that client has subscribed to the channel. So, the first thing you need to do is subscribe the channel. Binding to the event on the client:
pusher.bind('event_name', function( data ) {
// handle update
} );
This is also known as "global event binding".
I've tested this using this code and it does work:
http://jsbin.com/AROvEDO/1/edit
For completeness, here's the code:
var pusher = new Pusher('APP_KEY');
var channel = pusher.subscribe('test_channel');
pusher.bind('my_event', function(data) {
alert(data.message);
});

Storing and retrieving Sockets in Redis

I am new to Node and Redis, so forgive me if I'm missing something crucial here. I am trying to store a Node.js net.Socket object in Redis, using the node_redis client, so I could reuse a connection made previously on this Socket.
I am storing the socket as a Value as a part of a "users" set:
client.sadd("users", username); //username is a string
client.hmset(username, "ip", socket.remoteAddress, "connection", net.Socket(socket));
At a different point, I am retrieving this socket as:
client.smembers("users", function(err, replies) {
replies.forEach(function(reply,i){
if(recipient == reply) { //recipient is the username i've got
client.hget(reply, "connection", function(err, obj) {
console.log("socket's remoteaddress = " + net.Socket(obj).remoteAddress);
net.Socket(obj).write("asdasdasd");
});
}
});
});
But, the:
net.Socket(obj).remoteAddress); is logged as 'undefined'.
Also,
net.Socket(obj).write("asdasdasd"); gives me and error saying:
Error: This socket is closed.
So I guess my question is - can you store Sockets this way in Redis and expect them to work upon retrieval? Is there a correct / better way to do this?
P.S. I tried retrieving the Socket without the cast to net.Socket and it still didn't do any good.
Sockets and socket APIs (such as Berkeley Sockets) intrinsically handle special system-specific resources for which it just wouldn't make any sense to store or reference on a remote system, or even serialize for that matter.
Although, in reality, sockets are often uniquely identified by file descriptors so you could easily store that small, unique integer in a database for later access; however, that integer would only be meaningful during the active lifetime of that socket within that process instance. A strategy such as this one will be fraught with challenges, mainly having to do with the transient nature of the socket file descriptor number.
as #maerics said , redis store only string. so your socket data will be destroyed and will give undefined as answer.
you can store your socket like this.
var OBJECT = Object.prototype;
OBJECT.rhash = {};
OBJECT.rset = function(id, object) {
OBJECT.rhash[id] = object;
return id;
};
OBJECT.rget = function(id) {
return OBJECT.rhash[id];
};
this will store reference to that object and reuse it again..
var id = OBJECT.rset("a123",socket);
var ref = OBJECT.rget("a123");
console.log(ref.remoteAddress);
Regarding the IP address, it's on a different hash key (not connection, but ip). So to get the value you just read from that key:
client.hget(reply, "ip", function(err, obj) {
console.log("socket's remoteaddress = " + obj);
});

Resources