AppEngine channel API: duplicate messages client side - google-app-engine

I am trying to use the Channel API to push updates from server to the client. The flow is the user presses a button which triggers a server side action that generates a lot of logs. I want to display the logs to the user "in real time".
When I first load the page it I get all the messages, no problem. If I trigger the action a second time without refreshing the page in my browser, then all messages appear twice. Here is the set up portion of the channel that is tied to the page onLoad event. With resulting console logs I gathered that the onMessage() method is being invoked more than once when the page is not refreshed. Looks like I need to "kill" earlier sockets in some way, but could not find a way in the official documentation. Can someone point me in the right direction to get rid of the spurious messages?
// First fetch a token for the async communication channel and
// create the socket
$.post("/app/channels", {'op':'fetch', 'id' : nonce},
function (data, status, xhr) {
if (status == "success") {
data = JSON.parse(data);
token = data["token"];
console.log("Cookie: " + get_mp_did() + "; token: " + token);
var channel = new goog.appengine.Channel(token);
var handler = {
'onopen': onOpened,
'onmessage': onMessage,
'onerror': function() {
$("#cmd_output").append('Channel error.<br/>');
},
'onclose': function() {
$("#cmd_output").append('The end.<br/>');
$.post("/app/channels", {'op':'clear'});
}
};
var socket = channel.open(handler);
socket.onopen = onOpened;
socket.onmessage = onMessage;
}
});
onOpened = function() {
$("#cmd_output").empty();
};
onMessage = function(data) {
message = JSON.parse(data.data)['message'];
$("#cmd_output").append(message);
console.log('Got this sucker: ' + message);
}

If I understand your post and code correctly, the user clicks on a button which calls the $.post() function. The server code is responsible to create the channel in GAE as response to a /app/channels request. I think that your server in fact creates a new channel client ID / token with every subsequent request. Since the page is not reloaded, any subsequent request would add a new channel to this client. And all these channels would be still connected (hence, no page refresh).
I assume your server code has all channels associated to a user, and you send the message to a user utilizing all channels? Such pattern would result in this behavior. You can verify my assumption by clicking 3 or four times on the button with-out page refresh. The log output would be multiplied by the factor of 3 or 4.
I suggest, that you store the token in the client and on the server. Then make a modification to your client JS. If a channel is already created store the token value and provide it to any subsequent request to /app/channels. Modify the server so it will not create a new channel, if a token is provided with the request. If the token links to an existing valid channel, re-use the channel and return the same token in the response. You may need to add some more details for disconnected or expired channels, maybe also a cron-job to delete all expired channels after a while.

Related

Send user online/offline status to Laravel with socket.io

I have a chat app as real-time with socket.io. I want to save online/offline status on the database in Laravel back-end. For online status I don't have problem, but for offline status, I don't have any plan. My front-end framework is Angularjs 1.x and use JWT for authentication.
As matter of fact I want to save offline user status in database when user close browser of change route and emit all users from these user's status?
One possible solution would be just reacting to the socket.io server side events like the listening the socket connection and disconnect events.
This would the easiest solution, the problems would occur when your server crashes and you never receive the disconnect event. To avoid such problem, you could create interval check, which after every minute checks the opened sockets and users opened/closed in DB and updates if there changes.
Client side:
var socket = io("http://127.0.0.1:3000/", { query: "id=434354fds43" });
Server side:
var io = require('socket.io')(80);
var users = {};
io.use(function (socket, next) {
console.log('Query: ', socket.handshake.query);
var id = socket.handshake.query.id;
if (id) {
users[socket.id] = id;
return next();
}
// call next() with an Error if you need to reject the connection.
next(new Error('Authentication error'));
});
io.on('connection', function (socket) {
// New connection, get the ID from map
var id = socket.id;
socket.on('disconnect', function () {
// User left
delete users[id];
});
});

Socket.io page refresh disconnects from the room

I've been reading answers about this problem for some time now but none of the solutions seem to work for my setup.
I have a nodeJS server in conjunction with express. I use Socket.io to send notifications to individual users. (frontend is Angular)
When a user logs in, he joins a room named after his email address (unique).
io.on('connection', function (socket) {
socket.on('join', function(user) {
//list of connected users
connected_users.push({socket_id: socket.id, email: user.email});
socket.join(user.email);
});
...
The join event is broadcasted from angular when a user logs in.
This way I can send notifications like so simply by using email addresses:
io.sockets.in(to).emit('notification', {
message: msg,
source: from,
destination: to,
event: data
});
When a user manually logs out I register the following event listener:
socket.on('leave', function(user) {
//remove the user from the list
var index = findUserConnected(socket.id);
if(index != null) {
connected_users.splice(index, 1);
}
socket.leave(user.email);
});
And finally there's the disconnect handler for whenever a user logs out or refreshes the page:
socket.on('disconnect', function() {
//if the user refreshes the page, he is still in the connected users list
var email = findEmailUserConnected(socket.id);
if(email != null) {
//we make him join back his room
socket.join(email);
}
});
Technically this works. On page refresh, the user joins back his room.
The problem is only on page refresh, notifications sent using io.sockets.in(email).emit('notification', {}); are not received even though the user is in his room.
Apparently a page refresh calls socket.disconnect() which generates a new socket_id. I'm not sure if there's a way to reassign a socket_id to a room or something similar.
Ok first of all receiving a 'disconnect' event on server means that connection on that socket is going to terminate. So, there is no use for making that same socket join back in a room as you are doing right now.
socket.on('disconnect', function() {
var email = findEmailUserConnected(socket.id);
if(email != null) {
socket.join(email); // this would never work because this socket connection is not going to exist anymore.
}
});
My suggestion would be to make sure that the user always joins back into the room(email) every time a new connection is made. It can be easily done by adding sending the join event on every new connection.
In your client side code do
var socket = io();
socket.on('connect', function() { // 'connect' event is received on client on every connection start.
socket.emit('join', user); // where 'user' is your object containing email.
})
This way it ensures that whenever a new connection is established the join event is sent to server and the 'socket.on('join',...)' listener in your server will add the new socket to the room. Hope this helps :)

How do I send user specific data with socket.io and laravel?

I am not sure how to word this question right, but here I go. I have laravel, angular, node w/socket.io and I am also using JWT for authentication. My end goal is to be able to send real time update notifications to specific users. For the life of me, I cannot seem to get how the workflow would be.
My initial though was to send the jwt within the handshake and then use then in node to do http requests to get data, and then return said data. In another words, when a specific event is fired, node will already have the token, send request to laravel for specific information.
Can someone please explain to me how sending user specific data via socket.io in this architecture?
I found this great article : https://www.ukietech.com/blog/programming/step-by-step-instruction-of-setting-up-real-time-secure-broadcasting-with-laravel-5-1-socket-io-and-redis/
This set me on the right track.
First I need to pass in my JWT into the socket:
var socket = io('http://192.168.10.10:3000', {query: "Authorization="+$rootScope.$storage.satellizer_token});
Next I actually verify the token.. again. I know this may be overkill, but I want to know that what hits the socket is legit.
io.use(function(socket, next){
if (socket.handshake.query.Authorization) {
var config = {
url:'http://192.168.10.10/api/auth',
headers:{
Authorization:'Bearer '+socket.handshake.query.Authorization
}
};
request.get(config,function(error,response,body){
socket.userId = JSON.parse(body).id;
next();
});
}
// call next() with an Error if you need to reject the connection.
next(new Error('Authentication error'));
});
The request in this block of code returns a user object based on the authenticated token. Refer to JWTAuth for more.
Then on connection I will assign the user to a unique channel.
io.on('connection',function(socket){
socket.join('userNotifications.'+socket.userId);
console.log('user joined room: userNotifications.'+socket.userId);
});
Then broadcast the event:
notifications.on('pmessage', function(subscribed, channel, message) {
var m = JSON.parse(message);
io.emit(channel+":"+m.event, message);
});
Back on the client side I listen for the channel. the var user is the user id.
socket.on('userNotifications.'+ user+':App\\Events\\notifications', function(message){
console.log(message);
});

MEANJS: Security in SocketIO

Situation
I'm using the library SocketIO in my MEAN.JS application.
in NodeJS server controller:
var socketio = req.app.get('socketio');
socketio.sockets.emit('article.created.'+req.user._id, data);
in AngularJS client controller:
//Creating listener
Socket.on('article.created.'+Authentication.user._id, callback);
//Destroy Listener
$scope.$on('$destroy',function(){
Socket.removeListener('article.created.'+Authentication.user._id, callback);
});
Okey. Works well...
Problem
If a person (hacker or another) get the id of the user, he can create in another application a listener in the same channel and he can watch all the data that is sends to the user; for example all the notificacions...
How can I do the same thing but with more security?
Thanks!
Some time ago I stumbled upon the very same issue. Here's my solution (with minor modifications - used in production).
We will use Socket.IO namespaces to create private room for each user. Then we can emit messages (server-side) to specific rooms. In our case - only so specific user can receive them.
But to create private room for each connected user, we have to verify their identify first. We'll use simple piece of authentication middleware for that, supported by Socket.IO since its 1.0 release.
1. Authentication middleware
Since its 1.0 release, Socket.IO supports middleware. We'll use it to:
Verify connecting user identify, using JSON Web Token (see jwt-simple) he sent us as query parameter. (Note that this is just an example, there are many other ways to do this.)
Save his user id (read from the token) within socket.io connection instance, for later usage (in step 2).
Server-side code example:
var io = socketio.listen(server); // initialize the listener
io.use(function(socket, next) {
var handshake = socket.request;
var decoded;
try {
decoded = jwt.decode(handshake.query().accessToken, tokenSecret);
} catch (err) {
console.error(err);
next(new Error('Invalid token!'));
}
if (decoded) {
// everything went fine - save userId as property of given connection instance
socket.userId = decoded.userId; // save user id we just got from the token, to be used later
next();
} else {
// invalid token - terminate the connection
next(new Error('Invalid token!'));
}
});
Here's example on how to provide token when initializing the connection, client-side:
socket = io("http://stackoverflow.com/", {
query: 'accessToken=' + accessToken
});
2. Namespacing
Socket.io namespaces provide us with ability to create private room for each connected user. Then we can emit messages into specific room (so only users within it will receive them, as opposed to every connected client).
In previous step we made sure that:
Only authenticated users can connect to our Socket.IO interface.
For each connected client, we saved user id as property of socket.io connection instance (socket.userId).
All that's left to do is joining proper room upon each connection, with name equal to user id of freshly connected client.
io.on('connection', function(socket){
socket.join(socket.userId); // "userId" saved during authentication
// ...
});
Now, we can emit targeted messages that only this user will receive:
io.in(req.user._id).emit('article.created', data); // we can safely drop req.user._id from event name itself

Google Channel API sending message with token

In documents it says 'client_id' part can actually be the token, however it doesn't work. Anyone know why?
https://developers.google.com/appengine/docs/python/channel/functions
If the client_id parameter is actually a token returned by a create_channel call then send_message can be used for different versions of the app. For instance you could create the channel on the front end and then send messages from a backend of the app.
the reason i want to use this, is because i want to send messages to anonymous users as well, without requiring them to login. i don't know if it is possible to assign them a 'client_id' if token doesn't work.
this is how i am creating the token
user = users.get_current_user()
if user:
token = channel.create_channel(user.user_id())
else:
token = channel.create_channel(str(uuid.uuid4()))
then injecting into client
template_values = {
'token' : token,
}
on the client side open the channel
openChannel = function() {
var token = '{{ token }}';
var channel = new goog.appengine.Channel(token);
var handler = {
'onopen': onOpened,
'onmessage': onMessage,
'onerror': function() {},
'onclose': function() {}
};
var socket = channel.open(handler);
socket.onopen = onOpened;
socket.onmessage = onMessage;
}
now send a message
var xhr = new XMLHttpRequest();
xhr.open('POST', path, true);
xhr.send();
in the server,
when the message is received send back a message using the token
channel.send_message(token, someMessage)
back to client
onMessage = function(m) {
alert("you have some message");
}
this sequence works fine if client_id() is used instead of token when calling send_message
In response to btevfik's initial question: Allowing tokens or client_id in send_message is a feature released in 1.7.5 (very recently). Some people may not be familiar with it yet so therefore they suggest to use client_id. Both should work!
The only thing that I can see in your code is the fact that you should not rely on token variable to be correct in between two requests. They may not even land on the same instance of the app. If you share your code with more details I may be able to spot something. The proper way would be to either store the token in the datastore or pass it from the client as a parameter when you send the message that will trigger a message back.
The purpose of this feature was to allow people to send messages from backends (or other versions). Before was not possible whereas now you can do it if you use directly the tokens instead of the client_id.
Long time this post has been around, but just curious about your usage of the token global variable?
I don't see this code:
global token
before you set the token
user = users.get_current_user()
if user:
token = channel.create_channel(user.user_id())
else:
token = channel.create_channel(str(uuid.uuid4()))
If that code is missing, then token will be set in the local scope of the function above and not globally. So, the token value used later will be None (or to what ever the token was initialised with.)
Just a thought, if its still relevant.
I don't think you actually have a problem here.
You are able to send messages to users that are logged in or not.
The problem you are having I think is knowing that there are multiple ways to use the channel API re: tokens.
https://developers.google.com/appengine/docs/python/channel/overview#Life_of_a_Typical_Channel_Message
In this example, it shows the JavaScript client explicitly requests a token and sends its Client ID to the server. In contrast, you could choose to design your application to inject the token into the client before the page loads in the browser, or some other implementation if preferred.
This diagram shows the creation of a channel on the server. In this
example, it shows the JavaScript client explicitly requests a token
and sends its Client ID to the server. In contrast, you could choose
to design your application to inject the token into the client before
the page loads in the browser, or some other implementation if
preferred.
Here's my demo implementation, hope it helps somehow: https://github.com/Paul1234321/channelapidemo.git
Here's the code for creating the channel on GAE:
client_id = str(uuid.uuid4()).replace("-",'')
channel_token = channel.create_channel(client_id)
And in the JS:
channel = new goog.appengine.Channel('{{ token }}');
Have a look at it in action: http://pppredictor.appspot.com/
You shouldn't store request-specific values in global variables. Store them in a cookie or pass them as a request parameter instead.

Resources