Monday, July 1, 2013

Writing a real time single page application - server side

In the first part I highlighted how to take care of the frontend of a real time web app. Now I'll explain how to face the backend. As usual I will not dive into details, follow the links if you search in depth explanations.

Server side

I wrote the server side of my application using node.js and a very lightweight framework called express.js
The most important feature of this framework is the middleware system.
This is a middleware:

function middleware(req, res, next){
...
}

A middleware is a sort of russian doll. You can put a middleware inside another middleware (the "next" argument).

A middleware can basically:
  • call the next middleware in the chain ( next() )
  • call the output function ( res() )
  • change the input (req)
  • overwrite the output function (res)

You can use a middleware for:
  • authenticate and get user informations
  • route to a specific middleware using the URL (req.url) and the method (req.method)
  • add a specific header to the HTTP response
  • etc.

Using middlewares is very common because it allows to build simpler and reusable components.

Express.js gives you already a lot of middlewares but I used passportjs to get a complete solution for authentication.
In this example I will store users into couchdb using nano

var config = require('./config'), // I used a config.json to store configuration parameters
    db_url = config.db_protocol + "://" + config.db_user + ":" + config.db_password + "@" + config.db_url;
    nano = require('nano')(db_url), 
    setupAuth = require('./auth'),
    MemoryStore = express.session.MemoryStore,
    sessionStore = new MemoryStore(), // nano will store user id in this session
    passport = setupAuth(userdb);

var app = express(),
    server = http.createServer(app);
    
// configure Express
app.configure(function() {
    app.set('views', __dirname + '/views');
    app.set('view engine', 'ejs');
    app.use(express.logger());
    app.use(express.cookieParser());
    app.use(express.bodyParser());
    app.use(express.methodOverride());
    app.use(express.session({ store: sessionStore, secret: config.session_secret, key: config.session_key }))
    // Initialize Passport!  Also use passport.session() middleware, to support
    // persistent login sessions (recommended).
    app.use(flash());
    app.use(passport.initialize());
    app.use(passport.session());
    app.use(app.router);
    app.use(express.static(__dirname + '/' + config.static_dir));
});

var ensureAuthenticated = function(req, res, next) {
    if (req.isAuthenticated()) {
        return next();
    }
    res.redirect('/login');
};

// I check if a user is authenticated before accessing this URL
app.get('/', ensureAuthenticated, function(req, res){
  res.render('index', { user: req.user});
});

// login
app.get('/login', function(req, res){
    res.render('login', { user: req.user });
});

app.post('/login', 
    passport.authenticate('local', { failureRedirect: '/login', failureFlash: true }),
function(req, res) {
    res.redirect('/');
});

app.get('/logout', function(req, res){
  req.logout();
  res.redirect('/login');
});

server.listen(config.http_port);

The auth module contains functions used by passport:

var passport = require('passport'),
    LocalStrategy = require('passport-local').Strategy,
    crypto = require('crypto');


module.exports = function (userdb){
    var getUserFromId = function(id, done) {
        userdb.get(id, { revs_info: false }, function(err, body) {
            if (!err){
                return done(null, body);
            }
            else {
                return done(null, false, { message: 'Invalid credentials' });
            }
        });
    };

    passport.getUserFromId = getUserFromId;
    
    passport.serializeUser(function(user, done) {
        done(null, user._id);
    });
        
    passport.deserializeUser(getUserFromId);

    passport.use(new LocalStrategy(
        function(username, password, done) {
            var shasum = crypto.createHash('sha1').update(password),
                key = [username, shasum.digest('hex')]

            // this view's keys are [username, password]
            // the password is hashed of course
            userdb.view("user", "login", { keys: [key] }, function (err, body){
                if (!err) {
                    if (body.rows.length){
                        // logged in !!!
                        return done(null, body.rows[0].value);
                    }
                    else {
                        return done(null, false, { message: 'Invalid credentials' });
                    }
                }
                else {
                    console.error(err);
                    return done(null, false, { message: 'Db error, try later' });
                }
            });        

        })
    );

    
    return passport;
};

This example is explained in the passport documentation.
If you are careful you will notice that the getUserFromId function is called every time to get the complete object from the database (couchdb in this case).
This is not very optimal and it's better to cache users for some time. I used this nice memoization module:

    var memoize = require('memoizee');
    
    // cache this function for optimal performance (2 minutes)
    // https://npmjs.org/package/memoizee
    getUserFromId = memoize(getUserFromId, { maxAge: 120000,  async: true});

Backbone.io in the server

At this point I will define a backbone.io backend (as explained here: https://github.com/scttnlsn/backbone.io)

var items_backend = backboneio.createBackend();

A backend is very similar to an express.js middleware.

var backendMiddleware1 = function(req, res, next) {
   ...
};

items_backend.use(backendMiddleware1);
items_backend.use(backendMiddleware2);

Once defined, I will connect the backend.

var io = backboneio.listen(server, {items: items_backend});

The io object which is returned by the listen method is a socket.io object.

When a websocket is connected for the first time it makes a basic handshake. This phase can be used to perform the passport authentication.

var cookie = require('cookie'),
    cookiesig = require('cookie-signature');
// socket io authorization
io.set('authorization', function (data, accept) {
    if (data.headers.cookie) {
        data.cookie = cookie.parse(data.headers.cookie);

        // cookies are signed for better security:
        //
        // s:name:signature
        //
        // s: is a prefix for signed cookies
        // name is the cookie name
        // signature is an hmac of the value
        // doing this the client cannot change the cookie value
        // without invalidate the cookie
        if (data.cookie[session_key].indexOf('s:') === 0){
            data.sessionID = cookiesig.unsign(data.cookie[session_key].slice(2), session_secret);
        }
        else {
            data.sessionID = data.cookie[session_key];
        }
        
        // (literally) get the session data from the session store
        sessionStore.get(data.sessionID, function (err, session) {
            
            if (err || !session) {
                // if we cannot grab a session, turn down the connection
                accept('Cannot get sessionid', false);
            } else {
                // save the session data and accept the connection
                data.session = session;
                if("passport" in session && "user" in session.passport){
                    passport.getUserFromId(session.passport.user, function (err, user, message){
                        if(err || !user){
                            accept('Cannot find user', false);
                        }
                        else {
                            try{
                                data.user = user;

                                accept(null, true);
                            }
                            catch (e){
                                accept('Error: ' + e.toString(), false);
                            }

                        }
                        
                    });
                }
                else {
                    accept('Session does not contain userid', false);
                }
            }
        });
    } else {
       return accept('No cookie transmitted.', false);
    }
});

The tricky part here is to extract the session from the (signed) cookie. The passport and sessionStore objects are the same defined before for normal authentication.
The backend authentication middleware can get the object through req.socket.handshake object:

var authMiddleware = function(req, res, next) {
    var user = req.socket.handshake.user;

    if (!req.user){
        next(new Error('Unauthorized'));
    }
    else {
        req.user = user;
        next();
    }

};

Backend events and channels

When a backend changes something backbone.io automatically broadcasts the change to every node connected (and triggers the events I talked before).
You often need to notify only a subset of clients. For this reason you can define channels. Every changes will be notified to clients connected to a certain channel.
The channel can be defined client side but I added a useful feature to define channel server side, during the handshake.

There is also another case where you need to detect whether couchdb has been changed by another application.
In this case I recommend you to use my fork of backbone.io  because it supports channels.

Database and flow control with promises

The last piece of the application is talking to the database. In my application I used couchdb but it is really not important which database will you use.
In the first paragraph I have underlined that a single resource operation could cause many operations in the backend. For this reason is very important using a smarter way to control the flow. I have chosen promises.

Promises are a standard pattern to manage asynchronous tasks. I used this library: https://github.com/kriskowal/q
The advantage of promises is to avoid the "pyramid of doom" of nested callbacks:

step1(function (value1) {
    step2(value1, function(value2) {
        step3(value2, function(value3) {
            step4(value3, function(value4) {
                // Do something with value4
            });
        });
    });
});

And transform that in something more manageable:

Q.fcall(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
    // Do something with value4
}, function (error) {
    // Handle any error from step1 through step4
})
.done();

With promises, managing errors is very easy.
This is an example using nano:

var getUser = function (id) {
    var deferred = Q.defer();

    userdb.get(id, {}, function(err, body) {
        if (err) {
            deferred.reject(new Error('Not found'));
        }
        else {
            deferred.resolve(body);
        }
    });
    return deferred.promise;
};

var getGroup = function (user) {
    var deferred = Q.defer();

    userdb.get(user.groupid, {}, function(err, body) {
        if (err) {
            deferred.reject(new Error('Not found'));
        }
        else {
            deferred.resolve(body);
        }
    });
    return deferred.promise;
};

function getGroupFromUserId(id, callback){
    getUser(id)
    .then(getGroup)
    .then(function (group){
        callback(group);
    })
    .done();
}

Backbone.io and databases

Backbone.io has some ready to use backend and it is quite easy to write your own following the examples (I added the couchdb backend).

The end?

This ends this whirlwind tour. I hope someone will find this useful.