socket.io + hapi.js

You might already came across a new amazing web technology called web sockets, which enables the server to push data in an event-driven manner to many web clients. 5 minutes after reading what web sockets are, you may have found the amazing socket.io for node.js.

Setting up a basic example cannot be made easier. But what about integration into a web backend crafted with hapi.js or express?

Once again google comes to the rescue and there are a lot of tutorials describing how to combine socket.io with express. Now comes the bad part: Most of them were written for express 3 and socket.io < 1.0. Since then, a lot changed and so we decided to give a small insight on how we authorize our sockets in our backends and how we integrate them with the framework of our choice hapi.js.

This tutorial is made for hapi.js 8.x and socket.io 1.x. If you are using this combination, we would be more than pleased for feedback of any kind. But now let’s come to the interesting part:

Hapi enforces encapsulation via plugins, so all the business logic here is situated in our own plugin. The basic structure of a plugin requires some metadata like name and version and a register function, which has the following signature:

module.exports.register = function (server, options, next) {  
    //everything happens here
});

As easy as can be. We initialize socket.io in our plugin and bind it to the http server, so it will listen for upgrading connections at the specified path.

var io = require("socket.io")(server.listener, {  
    serveClient: false,
    path: "/socketpath",
    log: true
});

Now we have to listen for incoming sockets and tell them if we want to allow the connection or close it:

io.on("connection", function (socket) {  
    console.log("client incoming");
});

Since socket.io 1.0 we can do everything via middleware, which is called when a new socket connects and which gives us control on what to do with the socket. To allow a connection we just call next() and we want to refuse it we call the next function with a descriptive error as only argument.

io.use(function (socket, next) {  
    var cookies = socket.request.headers.cookie;

    //the authentication takes place here
});

We need to get the connection object, cause all cookie information, we have configured in the hapi server, are stored here. Hapi uses statehood under the hood, which decodes our iron-encoded cookies and gives us all the meta information we stored in the cookie.

server.select("<your descriptive server label>").connections[0].states.parse(cookies, function (err, state, failed) {  
        //if no session is set, connection will be refused
        if (!state.session) {
            return next(new Error("Cookie not found"));
        }

        //continue authentication
});

Now that we have the session cookie, we need to fetch the session from our session store. We are using the arango db session system and access it by our own coupling mechanism realised via hapi-intercom. Based on your session system the following lines may differ, but if you reached this point, you already have the most important puzzle pieces to have a good integration. First we get the arangodb channel to request a connection.

var channel = server.methods.intercom.getChannel("arangodb");  

The next step is to request a connection from the connection pool. Everything from here on is a promise to ease asynchronous data flow 😉

channel.request("connection")  

We can now fetch the session from the database via its API:

.then(function (conn) {
    return conn.foxx(options.foxxName, "session/" + state.session.id).get({});
})

Since Promises are thenable, we just chain everything we want to do to authenticate a socket. In the next step, we check if the session is authenticated. This is the case when the uid key is set. If this the case, we allow the connection and everything is fine and setup for some fancy real-time communication.

.then(function (result) {
    //the session object is stored under data in the response
    var session = result.data;

    //if the userid is not set, we are not authenticated
    if (!session.uid) {
        return next(new Error("Session is not authenticated"));
    }

    //everything is fine, session is authenticated and we accept the
    //connection
    next();
})

Even if this the shiny new web socket world, there might occur errors sometimes, so we also have to check for any exceptions during our authentication process and want to log it to the hapi framework. Since a socket event cannot be bound to a request, we will use the server.log feature to log information on any error occuring and of course we will refuse the socket connection, since something unexpected happened.

.catch(function (err) {
    //if any error is thrown during the authentication process, we log the
    //error and refuse authentication
    server.log(["socket", "error", "authentication"], {
        error: err,
        message: "Error during socket authentication for conversation channel"
    });
    return next(err);
});

This is a small roundup to illustrate the basic flow to integrate sockets in your session system. This works really well for us and we would love to hear some real life scenarios from you.

Have fun with real-time communication!

And for those who like orientation in sourcecode, here is the whole code glued together:

module.exports.register = function (server, options, next) {  
    var io = require("socket.io")(server.listener, {
        serveClient: false,
        path: "/socketpath",
        log: true
    });

    io.on("connection", function (socket) {
        console.log("client incoming");
    });

    io.use(function (socket, next) {
        var cookies = socket.request.headers.cookie;

        server.select("<your descriptive server label>").connections[0].states.parse(cookies, function (err, state, failed) {
            //if no session is set, connection will be refused
            if (!state.session) {
                return next(new Error("Cookie not found"));
            }

            //first we get the arangodb channel to request a connection
            var channel = server.methods.intercom.getChannel("arangodb");

            //now we are requesting a connection from the connection pool
            //everything from here on is a promise to ease asynchronous data flow ;)
            channel.request("connection").then(function (conn) {
                //we request the session from the database
                return conn.foxx(options.foxxName, "session/" + state.session.id).get({});
            }).then(function (result) {
                //the session object is stored under data in the response
                var session = result.data;

                //if the userid is not set, we are not authenticated
                if (!session.uid) {
                    return next(new Error("Session is not authenticated"));
                }

                //everything is fine, session is authenticated and we accept the
                //connection
                next();
            }).catch(function (err) {
                //if any error is thrown during the authentication process, we log the
                //error and refuse authentication
                server.log(["socket", "error", "authentication"], {
                    error: err,
                    message: "Error during socket authentication for conversation channel"
                });
                return next(err);
            });
        });
    });
});    

If you are new to node.js and want to get started, have a look here.

Posted in Uncategorized