Saturday, August 21, 2010

Per Message Authorization in Faye

‹prev | My Chain | next›

Tonight, I continue in my attempt to lock down faye messages. I want to ensure that only the original client can sent subsequent updates about players in my (fab) game. With a hack in place allowing me to add server-side faye extensions to the fab.js backend in my (fab) game, I am ready to go. Hopefully...

The other night, before I realized that I realized I needed the faye+fabjs server-side hack, I had made a first attempt at a server-side faye extension that locked messages to clients:
   var serverAuth = {
incoming: function(message, callback) {
// Let non-subscription messages through
if (message.channel !== '/meta/connect')
return callback(message);

// Get subscribed channel and auth token
var subscription = message.subscription,
msgToken = message.ext && message.ext.authToken;

// If the message has a player ID
if (message.id) {

// If the player is already in the room
if (players.get_player(message.id)) {
puts("[token check] " + players.get_player(message.id).token + " " + msgToken);

// If the tokens do not match, stop the message
if (players.get_player(message.id).token != msgToken)
message.error = 'Invalid player auth token';
}
}
// Call the server back now we're done
callback(message);
}
};
Not surprisingly (but still disappointedly), that does not work. There is no failure in the backend, but I see 400 errors in the client. Time to roll up the sleeves.

Print STDERR debugging shows that messages are not getting past the first line in my extension:
        // Let non-subscription messages through
if (message.channel !== '/meta/connect')
return callback(message);
Ah, damn! What was I thinking? I want to ignore all "/meta" channel connections. I have no idea why I tried to secure "/meta/connect" channels (confused while debugging tcpdump output?). No matter, I get that working:
    // Let non-meta messages through
if (message.channel.indexOf('/meta/') === 0)
return callback(message);
Better.

Better, but still not working. Not working, and still no errors in the backend. I should probably turn on debug logging, but I stick with print STDERR debugging:
//...

for (var prop in message) {
puts(" " + prop + ": " + message[prop]);
}
if (message.data.id) {
puts(" checking for player: " + message.data.id);

//...
That first puts message is seen when the player moves about the room. The "checking for player" message is not:
  channel: /players/create
data: [object Object]
clientId: 1ghc65q12m3s078kwna91givtxo
id: wo5i0717pqv49svmkbc1qvhhl
Ah, that id attribute is not the player ID, it is the message ID. That means that what I think of as the message, what will be sent on to connected clients, is in the data attribute:
    // If the message has a player ID
if (message.data.id) {

// If the player is already in the room
if (players.get(message.data.id)) {

// If the tokens do not match, stop the message
if (players.get(message.id).token != msgToken)
message.error = 'Invalid player auth token';
}
}
puts("here!!!!");
// Call the server back now we're done
return callback(message);
Even there, I still have an error where I am trying to retrieve a player based on the message ID instead of the player ID:
        // If the tokens do not match, stop the message
if (players.get(message.player.id).token != msgToken)
message.error = 'Invalid player auth token';
What surprised me about that (and several other) failures is that the game did not crash, but swallowed the errors silently. When I try to lookup a player by the wrong ID, players.get() returns undefined. Accessing the tocken attribute on undefined should throw an error. In fact, the function seems to crash—subsequent print STDERR statements are not executed—but the node.js remains running silently swallowing would be errors. That behavior is something that I need to investigate (tomorrow—could be an old version of node?).

For now, I plow on through a few more silent failure / silly mistakes and eventually get my extension working with:
var serverAuth = {
incoming: function(message, callback) {
// Let non-meta messages through
if (message.channel.indexOf('/meta/') === 0)
return callback(message);

// Get subscribed channel and auth token
var subscription = message.subscription,
msgToken = message.ext && message.ext.authToken;

// If the message has a player ID
if (message.data.id) {

// If the player is already in the room
if (players.get(message.data.id)) {

// If the tokens do not match, stop the message
if (players.get(message.daa.id).token != msgToken) {
puts("rejecting mis-matched token message");
message.error = 'Invalid player auth token';
}
}
else {
message.data.authToken = msgToken;
}
}
// Call the server back now we're done
return callback(message);
}
};
The client-side faye extension that adds the auth token is similar, though it defines an outgoing callback:
  var clientAuth = {
outgoing: function(message, callback) {
// Leave non-data messages alone
if (message.channel.indexOf('/meta/') === 0)
return callback(message);

// Add ext field if it's not present
if (!message.ext) message.ext = {};

// Set the auth token
message.ext.authToken = self.uniq_id;

// Carry on and send the message to the server
return callback(message);
}
};
this.faye.addExtension(clientAuth);
With that, I can no longer move player Fred from Bob's Javascript Console and I get a nice little warning on the server side:



Yay!

It was a little touch and go there for a while. There were times at which I was not even sure it was possible, but I finally got it working. I still have a less-than-ideal hack to add faye server-side extensions in my custom built list_with_faye (fab) app. I doubt that this works if I reload the page. I am still more than a bit concerned about node silently swallowing errors.

But I still call it a night happy.


Day #202

No comments:

Post a Comment