Count number of online users with nodeJS and Socket.io without duplicates - javascript

I'm having trouble displaying the correct amount of online users. A question similar to this have shown me that I can count the users this way:
var count = 0
socket.on('connection', function(client) {
count++;
client.broadcast({count:count})
client.on('disconnect', function(){
count--;
})
})
The issue I'm constantly running into with that method is that when a user happens to reload the page too quickly, the counter pulls in too much it can throw out.
As you can see, on the right side of the image, a user spammed the reload and it caught more users online than there actually is. (There was only one user on the server at this time)
My question is is there a better or more reliable way to count the exact amount users online without the extra 'virtual users', without using the users++/users-- method?

If they're logging in as a user, then you should authenticate them to the socket. Use that authentication to see if they already have a session, and disconnect them decrementing the count, before you increment it again with the new session.
An example below. The clients objects stores the connected clients, with values being the sockets they're connected to.
var clients = {};
socket.on('connection', function(client) {
//Authenticate the client (Using query string parameters, auth tokens, etc...), and return the userID if the user.
var userId = authenticate(client);
if ( !userId ) {
//Bad authentication, disconnect them
client.disconnect();
return;
}
if (clients[userId]) {
//They already have a session, disconnect
clients[userId].disconnect();
}
//Set session here
clients[userId] = client;
client.broadcast({count: Object.keys(clients).length})
client.on('disconnect', function(){
delete clients[userId];
})
})

Could do this pretty cleanly with the Observable pattern (using RxJS v5 here):
const { Observable } = require('rxjs')
const connections = Observable.fromEvent(socket, 'connection').mapTo(1)
const disconnections = Observable.fromEvent(socket, 'disconnect').mapTo(-1)
// emit 1 for connection, -1 for disconnection
Observable.merge(connections, disconnections)
.scan((total, change) => total + change, 0) // emit total
.subscribe(count => client.broadcast({ count }))

You can use Server Sent Events for this purpose really.
Take a look at https://javascript.info/server-sent-events

Related

How to block an incoming socket connection if the client already has one

So i noticed that you can run 'io()' in console on client side.
I'm worried that if someone were to loop it, it would crash the node.js server.
Does anybody know how to prevent multiple connection for the same user.
It is a fairly complicated process to do that properly.
But on that same note, people won't be able to crash your server with socket.io as easy as you think they would be able to.
Node.js can handle a ton of connections at once, same with socket.io. Obviously these are both dependent on what your server actually is; but even as Raspberry Pi can handle a significant amount of connections.
But, if you truly must implement this, I'd recommend checking out this issue and just making a counter-based dictionary of IP's and to disconnect sockets if their IP goes above a specific number.
Get the client's IP address in socket.io
Very crude, but it would do what you need.
you need some helper function on server side
get user ip with this package:
npm install request-ip
create array of users:
let users = [ ];
validate and add user to array on new join request
const requestIp = require('request-ip');
const addUser = () => {
const ipMiddleware = function(req, res) {
const clientIp = requestIp.getClientIp(req);
};
const existingUser = users.find(user.clientIp === clientIp)
if (existingUser) {
return false
}
const newUser = { clientIp };
users.push(newUser)
return true
}

Socket IO and Dynamic Rooms

If anyone is experienced with Websockets / Socket IO hopefully you can point me in the right direction.
I made a Discord Clone and I'm trying to optimize it to scale better. Right now when a user sends a message I query the DB for all users part of that server, and emit a message to their specific socket. This is obviously not going to scale well as every message requires a expensive query and lookup in the client list
// Emit messages to only users part of specific server
// Will only return list of users part of server and active in last 10 minutes
sqlQuery = `SELECT userservers.user_id FROM userservers
JOIN users ON users.user_id = userservers.user_id AND users.user_last_active > (NOW() - INTERVAL 10 minute)
WHERE server_id = ${sql.escape(serverId)}`;
const users = await sql.query(sqlQuery);
action = { type: "message", payload: msg };
// Iterate over users, and find them in clients list
// Emit over socket only to that user
users.forEach((user) => {
clients.forEach((client) => {
if (client.userId === user.user_id) {
io.to(client.id).emit(user.user_id, action);
}
})
});
However using Rooms for each Sever would eliminate my need to query the DB. I understand I can do this when the socket server first starts
// Get server list from Mysql DB
servers.forEach((server) => {
socket.join(server.name);
}
However my issue becomes, when a user create a new server once the application is already running It will not update the list.
I am probably missing some concept on creating dynamic rooms.
EDIT : I am thinking the solution could be that every time a "server" is created, I send a message to the socket server so it can join that "room"
Right now when a user sends a message I query the DB for all users part of that server
I think you can submit broad cast message to all online users, so instead of forEach client => io.to(clientId) you can submit broad cast message to all connected users io.emit('some event', { for: 'everyone' });
also I'm wondering why you are creating many servers? you can divide your server into namespaces by using const namespace = io.of('/thisIsASeparateNamespace'); and also you can submit broadcast messages to all users inside this name space by namespace.emit('some event', { for: 'everyone in name space' });
So your chat structure can be like this
Server
Namespaces // for separate chat app / or like slack work spaces
Rooms // for group chatting
ClientID // for one to one

Socket.io reconnection causes new username

I am creating a chat in node.js and socket.io.
When a user opens my application, I ping the server and ask it to let the user join a specific room and set a random username (which can be edited at a later stage).
I use this
io.on('connection', (socket) => {
socket.on('join room', (roomId, callback) => {
// leave and join room
if (socket.roomId) {
socket.leave(socket.roomId);
}
socket.roomId = roomId;
socket.join(roomId);
// set username
if (!socket.username) {
socket.username = getRandomUsername();
}
});
});
It seems to work, but I have experienced situation where I have had the application running for a long time, and suddenly the user's username has changed.
I am suspecting this to occur because the user has lost connection, and socket.io have tried reconnecting. When the user has reconnected, the original socket is lost (and therefore it has no username), and a new username is assigned to the new socket, which overrides the original client socket.
Is there a problem with my code by assigning a random username to the user like this? It is somehow the same Google Docs does when the user is not signed in (they often give you an animal's name).
How can I solve the problem? I would very much like to avoid using cookies. An option could probably be to tell the client that it lost connection, and it has to reload the page, and then ask the server to get the same username.
I already save the assigned username on the client with this code:
const socket = io();
let username = null;
socket.on('connect', function() {
socket.emit('join room', roomId, function (newUsername) {
username = newUsername;
});
});
(...)

socket.io assigning custom socket.id

I keep track the list of every users connected in the array.
So if there is a new connection, it will check whether the user is already on the list or not, if he was already on the list, then assign their socket.id with the corresponding socket.id on the list, otherwise just add them to the list.
It's for preventing same user counted as 2 user while he attempt to do multi-login.
Object.keys(client).forEach(function (key) {
if (client[key].id == data.id){
is_connected = true;
socket.id = key;
}
});
I have no problem handling the messages/chat that was sent/received by the user who attempt multi-login.
socket.on('chat', function(msg){
var data = {"name": client[socket.id].name, "message": msg};
io.emit('chat', data);
});
The io.emit for the chat message was succesfully sent to the user who attempting multi-login.
The problem I got was whenever the user decide to logout/disconnect from the server.
io.emit('user_leave', client[socket.id].id);
[Multi-Login Case] -> Multi-User and Dual-User are same user attempting Multi-Login
Whenever the Main-User disconnected from the server, the Dual-User received 'user_leave' sent by the server, because io.emit supposed to send it to all sockets.
But not otherwise, while the Sub-User disconnected from the server, the Main-user do not receive 'user_leave' emitted by the server.
*Note: Main-User is login first, then the Dual-User. So the Main-User information was saved directly in the array, while the Sub-User socket.id was assigned with the Main-User socket.id
[Update]
B2 socket.id was assigned with B1 socket.id, the io.emit for chat work perfectly while io.emit for disconnect only emitted to All except Dual-User(B2)
socket.id is used internally by socket.io for its own socket list. You cannot overwrite that or you break some of its ability to maintain its own data structures.
You have two choices:
You can use the existing socket.id value as is (without overwriting it) so you don't break existing behavior. It is already guaranteed to be unique on the server.
You can use a different property name for your own id such as socket.userId and then you won't conflict.
If you need to, you can maintain a map between your own custom id and the socket.io socket.id so you could get to one from the other.
Similar question here: Socket.io custom client ID
generateId prop of io.engine object can be used for to set the custom id.
Using this way, the all socket ids can be created on the server side without any issue.
Actually I wrote an answer for a similar question today.
An example:
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
io.engine.generateId = function (req) {
// generate a new custom id here
return 1
}
io.on('connection', function (socket) {
// listing the default namespace rooms
console.log("rooms: ", io.nsps["/"].adapter.rooms);
})
The console output would be as the following:
rooms: { '/#1': Room { sockets: { '/#1': true }, length: 1 } }
It seems to be it has been handled.
It must be in mind that socket id must be unpredictable and unique value with considering security and the app operations!
Extra: If socket.id is returned as undefined because of your intense processes on your generateId method, async/await combination can be used to overcome this issue on node.js version 7.6.0 and later. handshake method of node_modules/engine.io/lib/server.js file should be changed as following:
former:
Server.prototype.handshake = function (transportName, req) {
var id = this.generateId(req);
...
}
new:
Server.prototype.handshake = async function (transportName, req) {
var id = await this.generateId(req);
...
}

How can I prevent malicious use of my sockets?

I'm making a webpage based around players being able to invite other players to parties, and other things a long the lines.
I have your basic send / receive / update of the chat/users in your party. The only thing is, what's to stop somebody from sitting there opening up a developer console and going
socket.emit('updateUsers', 'Weiner');
socket.emit('updateUsers', 'Idiot');
socket.emit('updateUsers', 'Bad word');
socket.emit('updateUsers', 'Other stupid malicious really long spam the chat name');
How can I prevent against this so that they can not do such things?
(Full JS Stack, Node.js)
Thanks!
I faced this problem aswell, This was my solution as far as spamming emits go (malicious socket use)..
var spamData = new Object();
var spamCheckFunctions = ["updateUsers","moreEmits"]; // anti-spam will check these socket emits
var antiSpam = 3000; // anti spam check per milliseconds
var antiSpamRemove = 3; // -spam points per antiSpam check
var maxSpam = 9; // Max spam points before disconnect is thrown to the socket
io.sockets.on('connection', function (socket) {
// Spam Check, this binds to all emits
var emit = socket.emit;
socket.emit = function() {
data = Array.prototype.slice.call(arguments);
if(spamCheckFunctions.contains(data[0])){
addSpam(socket);
};
emit.apply(socket, arguments);
};
var $emit = socket.$emit;
socket.$emit = function() {
data = Array.prototype.slice.call(arguments);
if(spamCheckFunctions.contains(data[0])){
addSpam(socket);
}
$emit.apply(socket, arguments);
};
});
function maxSpamCheck(socket){
if(spamData[socket.username].spamScore>=maxSpam && !socket.spamViolated){
socket.spamViolated = true;
socket.disconnect();
}
}
function checkSpam(){
for(user in spamData){
if(spamData[user].spamScore>=1) spamData[user].spamScore-=antiSpamRemove;
}
return;
}
setInterval(checkSpam,antiSpam);
function addSpam(socket){
if(socket.spamViolated) return;
spamData[socket.username].spamScore+=1;
maxSpamCheck(socket);
}
// Then add this where your user is authenticated
function authenticate(socket){
socket.username = username // here you define username
socket.spamViolated = false;
spamData[socket.username] = {
spamScore: 0
}
}
Array.prototype.contains = function(k) {
for(var p in this)
if(this[p] === k)
return true;
return false;
};
basically binds to all emits and checks if the emit name is contained in spamCheckFunctions if it is it will add a spam point, if a user exceeds a spam score amount (maxSpam); he will be disconnected. And for every milliseconds defined at antiSpam will minus the user spam score defined at antiSpamRemove
I'm sure there are cleaner solutions but this one worked out pretty good for me :)
Just make sure to verify/authenticate the users.
this is how I authenticate them (not using nodejs as a webserver, but had django):
io.configure(function(){
io.set('authorization', function(data, accept){
if(data.headers.cookie){
data.cookie = cookie_reader.parse(data.headers.cookie);
return accept(null, true);
}
return accept('error', false);
});
});
now you can access socket.handshake.cookie['sessionid'] (in my case this worked with django)
then match the socket.handshake.cookie['sessionid'] with a entry where your sessions are stored on the webserver
That's a difficult problem in general. Two things you could do:
1) Use self-invoking functions on the client side, i.e.
(function(w) {
// define your sockets here
var socket = ...;
})(window);
Obviously it is on the client side so this is not really secure. But it's not bad to have such wall.
2) On the server side keep track of the frequency of posting. For example if someone posts 5 times in a second, then you can assume that it is a spam and you could block that socket. It is especially effective if combined with authentication and complex registration (so people will have problem in creating new account).
Use an md5/sha key which behaves like a cookie.
Generate a key for a specific user and send it to the client and always check that incoming requests have the same key
It won't be completely secure since the hacker can always find your key in either the source code or localStorage but try to hide it through obfuscation of your code
A way to prevent spam is to either implement user authentication and/or a packet rate limiter. Add a middleware function which keeps track of the socketId and the amount of packets being sent through that socket. When it exceeds your limit disconnect the socket.
You can even add an extra function which keeps track of the IP address of that socket, if an IP address will be disconnected too often due spam you can ban that ip. Add a check on your connection event which IP addresses are allowed.
Use rate-limiter-flexible package for limiting number of events per second. Limiting by IP is the most simple, but would be better to limit by userId if possible.
const app = require('http').createServer();
const io = require('socket.io')(app);
const { RateLimiterMemory } = require('rate-limiter-flexible');
app.listen(3000);
const rateLimiter = new RateLimiterMemory(
{
points: 5, // 5 points
duration: 1, // per second
});
io.on('connection', (socket) => {
socket.on('bcast', async (data) => {
try {
await rateLimiter.consume(socket.handshake.address); // consume 1 point per event from IP
socket.emit('news', { 'data': data });
socket.broadcast.emit('news', { 'data': data });
} catch(rejRes) {
// no available points to consume
// emit error or warning message
socket.emit('blocked', { 'retry-ms': rejRes.msBeforeNext });
}
});
});
Read more in official docs

Categories