Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge remote branch 'josem/master'

Conflicts:
	lib/juggernaut/client.js
	public/application.js
  • Loading branch information...
commit 7d015dd5c92da2f0a2ca82c54fd357df6e965fab 2 parents 5ec5081 + 7c79b0c
Michael Dotterer mdotterer authored
42 README.md
View
@@ -392,6 +392,48 @@ So, as to the JavaScript side to the observer, we need to subscribe to a observe
var jug = new Juggernaut;
jug.subscribe("/observer/" + user_id, process);
+##Security
+By default anyone can subscribe to any channel. If it's ok for you, just skip this section and everything will work as expected.
+
+If the security is a problem we provide an easy solution: signatures, which means that when a client wants to subscribe to a channel must provide a signature and a timestamp:
+
+ jug.subscribe(current_room, function(data) {
+ console.log("Got data: " + data);
+ }, signature, timestamp);
+
+This signature is just a string, encrypted in SHA1, generated from a secret token, the name of the channel we want to subscribe to and a timestamp separated by `:`. That means something like this: `token:channelName:timestamp`. In Ruby it's very easy to generate a proper signature:
+
+ timestamp = (Time.now.to_f * 1000).round
+
+ pre_signature = [SHARED_TOKEN, channel_name, timestamp].join(':')
+
+ signature = Digest::SHA1.hexdigest(pre_signature)
+
+
+The token should be saved separately on the server and the client. The timestamp and the signature are required when we subscribe to a channel if we activate the secure mode.
+
+In order to activate it, we should add the `-s` or `--security` option and a path of a file when we start Juggernaut.
+
+That file must follow this format:
+
+ var Config = {
+ // Token shared with the server. Required, since it's not defined by default
+ sharedToken: 'juggernautrocks',
+
+ // Time until a signatures expires, in seconds. Optional, 40 seconds by default
+ expiration: 40,
+ };
+
+ module.exports = Config;
+
+Now we can start Juggernaut with:
+
+ $ juggernaut --security 'path/to/file.js'
+
+Or:
+
+ $ juggernaut -s 'path/to/file.js'
+
##Full examples
You can see the full examples inside [Holla](http://github.com/maccman/holla), specifically [roster.rb](https://github.com/maccman/holla/blob/original/app/models/roster.rb), [juggernaut_observer.rb](https://github.com/maccman/holla/blob/original/app/observers/juggernaut_observer.rb) and [application.juggernaut.js](https://github.com/maccman/holla/blob/original/app/javascripts/application.juggernaut.js).
6 client.js
View
@@ -3251,7 +3251,7 @@ Juggernaut.fn.write = function(message){
this.io.send(message);
};
-Juggernaut.fn.subscribe = function(channel, callback){
+Juggernaut.fn.subscribe = function(channel, callback, signature, timestamp){
if ( !channel ) throw "Must provide a channel";
this.on(channel + ":data", callback);
@@ -3260,7 +3260,9 @@ Juggernaut.fn.subscribe = function(channel, callback){
var message = new Juggernaut.Message;
message.type = "subscribe";
message.channel = channel;
-
+ message.signature = signature;
+ message.timestamp = timestamp;
+
this.write(message);
});
6 client/vendor/assets/javascripts/juggernaut.js
View
@@ -47,7 +47,7 @@ Juggernaut.fn.write = function(message){
this.io.send(message);
};
-Juggernaut.fn.subscribe = function(channel, callback){
+Juggernaut.fn.subscribe = function(channel, callback, signature, timestamp){
if ( !channel ) throw "Must provide a channel";
this.on(channel + ":data", callback);
@@ -56,7 +56,9 @@ Juggernaut.fn.subscribe = function(channel, callback){
var message = new Juggernaut.Message;
message.type = "subscribe";
message.channel = channel;
-
+ message.signature = signature;
+ message.timestamp = timestamp;
+
this.write(message);
});
58 lib/juggernaut/authorization.js
View
@@ -0,0 +1,58 @@
+var sys = require("sys");
+var crypto = require("crypto");
+var redis = require("redis");
+var Config = require('./config');
+
+Authorization = module.exports = {};
+
+Authorization.redisClient = redis.createClient();
+
+Authorization.recordSignature = function(client, signature) {
+ var redisClient = this.redisClient;
+
+ redisClient.setnx(signature, 1, function(err, res) {
+ if (res === 1) {
+ redisClient.expire(signature, 90);
+ sys.log("Recording signature " + signature + " as used.");
+ } else {
+ sys.log("Signature " + signature + " has already been used! Issuing client disconnect.");
+ // Bump the juggernaut client off for reusing a token
+ client.disconnect();
+ }
+ });
+};
+
+Authorization.expiredSignature = function(timestamp) {
+ var signatureAge = Math.round(new Date().getTime()/1000.0) -
+ Math.round(parseInt(timestamp, 10) / 1000.0);
+
+ return signatureAge > Config.expiration;
+};
+
+Authorization.validateSignature = function(client, message) {
+ // If we are not in secure mode validateSignature always returns true
+ if (!Config.secure_mode) {
+ return true;
+ }
+
+
+ var channelName = message.getChannel(),
+ signature = message.getSignature(),
+ timestamp = message.getTimestamp(),
+ hash = crypto.createHash('sha1');
+
+ if (this.expiredSignature(timestamp)) {
+ return false;
+ }
+
+ this.recordSignature(client, signature);
+
+ hash.update([Config.sharedToken, channelName, timestamp].join(':'));
+
+ if (signature === hash.digest('hex')) {
+ return true;
+ } else {
+ sys.log("Signature mismatch!");
+ return false;
+ }
+};
31 lib/juggernaut/client.js
View
@@ -1,6 +1,9 @@
var util = require("util");
var Channel = require("./channel");
var Events = require("./events");
+var crypto = require("crypto");
+var Config = require('./config');
+var Authorization = require("./authorization");
Client = module.exports = require("./klass").create();
@@ -9,39 +12,45 @@ Client.include({
this.connection = conn;
this.session_id = this.connection.session_id;
},
-
+
setMeta: function(value){
this.meta = value;
},
-
+
event: function(data){
Events.custom(this, data);
},
-
- subscribe: function(name){
- util.log("Client subscribing to: " + name);
-
- var channel = Channel.find(name)
+
+ subscribe: function(message){
+ var channelName = message.getChannel();
+ util.log("Client subscribing to: " + channelName);
+
+ if (Config.secure_mode && (!Authorization.validateSignature(this, message))){
+ util.log("Client subscription authorization failed for: " + channelName);
+ return false;
+ }
+
+ var channel = Channel.find(channelName)
channel.subscribe(this);
},
-
+
unsubscribe: function(name){
util.log("Client unsubscribing from: " + name);
var channel = Channel.find(name);
channel.unsubscribe(this);
},
-
+
write: function(message){
if (message.except) {
var except = Array.makeArray(message.except);
if (except.include(this.session_id))
return false;
}
-
+
this.connection.write(message);
},
-
+
disconnect: function(){
// Unsubscribe from all channels
Channel.unsubscribe(this);
27 lib/juggernaut/config.js
View
@@ -0,0 +1,27 @@
+/*
+ Configuration file, all the variables will be avaliable
+ with Config.whatever, if we've exported and required this file
+*/
+
+var Config = {
+
+ /* This config is required whether we are in secure mode or not,
+ so if we are not in secure mode, we can run things as usual
+ checking that secure_mode is false
+ */
+ secure_mode: false,
+
+ /* This is required in secure mode, and it
+ must be the same string you'll have on the server, like:
+
+ sharedToken = 'e91d6517cbebe25100607b797f9c2fdc60d2df61',
+
+ By defualt it's not defined.
+ */
+ sharedToken: '',
+
+ // The time it takes until a signature expires, in seconds
+ expiration: 40,
+};
+
+module.exports = Config;
2  lib/juggernaut/connection.js
View
@@ -20,7 +20,7 @@ Connection.include({
switch (message.type){
case "subscribe":
- this.client.subscribe(message.getChannel());
+ this.client.subscribe(message);
break;
case "unsubscribe":
this.client.unsubscribe(message.getChannel());
8 lib/juggernaut/message.js
View
@@ -21,4 +21,12 @@ Message.prototype.getChannels = function(){
Message.prototype.getChannel = function(){
return(this.getChannels()[0]);
+};
+
+Message.prototype.getSignature = function(){
+ return(this.signature);
+};
+
+Message.prototype.getTimestamp = function(){
+ return(this.timestamp);
};
3,347 public/application.js
View
1 addition, 3,346 deletions not shown
40 server.js
View
@@ -1,5 +1,6 @@
#!/usr/bin/env node
var argv = require("optimist").argv,
+ path = require("path"),
util = require("util");
var help = [
@@ -8,14 +9,47 @@ var help = [
"Starts a juggernaut server using the specified command-line options",
"",
"options:",
- " --port PORT Port that the proxy server should run on",
- " --silent Silence the log output",
- " -h, --help You're staring at it"
+ " --port PORT Port that the proxy server should run on",
+ " --silent Silence the log output",
+ " -s, --security FILE File to add security, with a token and a time for the expiration of signatures",
+ " -h, --help You're staring at it"
].join('\n');
if (argv.h || argv.help) {
return util.puts(help);
}
+var config_path = (argv.s || argv.security) || false;
+
+if (config_path) {
+ // Secure mode, let's read the file if it exists
+ path.exists(config_path, function (exists) {
+ if (!exists) {
+ throw new Error('The config file does not exist, so we can\'t be in secure mode. '
+ + 'See the security section in the documentation for more details.');
+ }
+
+ var custom_config = require(config_path);
+
+ // Get the default values
+ Config = require("./lib/juggernaut/config");
+
+ // If the sharedToken has not been defined we throw
+ // an error since is required in secure mode
+ if (!custom_config.sharedToken) {
+ throw new Error('The shared token must be defined. See the security section in '
+ + 'the documentation for more details.');
+ }
+
+ Config.sharedToken = custom_config.sharedToken;
+
+ if (custom_config.expiration) {
+ Config.expiration = custom_config.expiration;
+ }
+
+ Config.secure_mode = true;
+ });
+}
+
Juggernaut = require("./index");
Juggernaut.listen(argv.port);
Please sign in to comment.
Something went wrong with that request. Please try again.