Tuesday 23 July 2013

Part 2 - Writing a Philips Hue Simulator in Javascript - Routing requests

In part 1, I introduced the Hue, talked briefly about the developer API and pulled together some Javascript technologies to build a simulator framework. In this post, we'll implement some code to handle incoming requests.

Exposing the API...


In the previous post, I talked about the main design features of the simulator - first on the list was "Respond to web service requests". As the API is exposed via a web service, this means the routing of the incoming HTTP requests. The majority of the code to do that is provided by the restify library so we just need to organise how we want our simulator to handle the requests and where we put that code.

Philips have made the job easier because they've split the API functionality into several logical areas; lights, groups, schedules, configuration and portal. It makes sense for us to keep this structure so we'll route the HTTP requests to corresponding "controller" modules. To do this, we'll add a module specifically for routing the incoming requests (route.js) and then some modules to perform the request actions (lights.js, groups.js, schedules.js, configuration.js and portal.js).

First, we'll modify the main module we created previously (app.js) to add support for the routing:
var restify = require('restify');

app = module.exports = restify.createServer({
 name: "Hue Bridge Simulator"
});

routes = require('./routes'); // <-- added

app.listen(8080, function() {
  console.log('%s listening at %s', app.name, app.url);
});
And we'll add the new routing module that it's depending on to routes.js:
var lightsController = require('./controllers/lights');
var groupsController = require('./controllers/groups');
var schedulesController = require('./controllers/schedules');
var configController = require('./controllers/configuration');
var portalController = require('./controllers/portal');

app.post('/api/:username', configController.register);
As you can see in the last line above, I've added a handler for the request which registers a new user for the bridge. We need to implement that in the corresponding controller file configuration.js. Note that the controller modules reside in a controllers subdirectory:
exports.register = function(request, response){
 console.log("register - user " + request.params.username);
};

Danny-boy, Danny-boy. Broadsword calling...


To test our simulator, we need to generate a suitable client HTTP request. There are lots of ways we can do this but one of the easiest is to use the curl command line tool. With the server (app.js) running with node.js, type the following in a separate command prompt:

C:\curl> curl -is -X POST http://localhost:8080/api/loada

And you should see the following from our simulator:

C:\> node app.js
Hue Bridge Simulator listening at http://0.0.0.0:8080
register - user loada

Notice that this doesn't really do anything at the moment apart from indicate the parameter (username) that's been supplied as part of the request. This brings us neatly onto another decision point - how are we going to store our data? In this case, we need to keep track of registered users so that we can authenticate the requests coming into the bridge.

Persistence pays off...


Now we need some mechanism that will persist our bridge data between requests. At this early stage, I think a simple JSON file will do the trick - we can always swap it out later for something a bit more heavyweight should we need it.

If your name's not down, you're not coming in...


The Hue API requires the caller to be on an authenticated "whitelist" of registered clients. The bridge maintains this list and only allows registration of a new client if a physical button on the top of the device is pressed - obviously, this requires a real person to be local to the device so its quite an effective security measure. To allow the simulator to keep track of users, we'll require a model (in the MVC sense) of the data. One of the easiest ways is to simply write your data to a JSON file and then read it back in when you require it - we'll store a file called whitelist.json.

In order to keep the model data separate from the rest of the simulator code, data will be abstracted into relevant modules and placed in a models subdirectory. To encapsulate the whitelist file, we'll create a module that offers a simple interface to maintain the list. This abstraction will help if we decide to go for a more robust storage solution later on and it also keeps the controller code focused purely on managing the incoming requests and outgoing responses. Our new module, users.js, looks like this:
var fileSystem = require('fs');
 
var whitelist = [];
var fileName = "./whitelist.json"
 
exports.getUsers = function() {
 return whitelist;
};

exports.addUser = function(username) {
 if (!checkUser(username)) {
  whitelist.push(username);
  saveUsers();
 } 
};

exports.checkUser = function(username) {
 return whitelist.indexOf(username) > -1;
};

function saveUsers() {
 fileSystem.writeFile(fileName, JSON.stringify(whitelist));
};

function loadUsers() {
 fileSystem.readFile(fileName, function(err, data) {
  whitelist = JSON.parse(data);
 });
};

loadUsers();
Nothing too groundbreaking in here - it offers some simple interface functions that manage the internal array of usernames (our so-called "whitelist") and then we save to (and load from) the JSON file. On an implementation sidenote, we'll take advantage of the ability to pass variables to modules through the require call - passing the users model over to the routing module to allow us to perform user authentication there - I'll explain more about that shortly. For now, we need to add the model to the main app.js module:
var restify = require('restify');

app = module.exports = restify.createServer({
 name: "Hue Bridge Simulator"
});

var users = require('./models/users'); // <-- added

routes = require('./routes')(users); // <-- note the parameter

app.listen(8080, function() {
 console.log('%s listening at %s', app.name, app.url);
});

Routing requests...


In order to perform validation and authentication on each client request, we'll modify the routes.js module that we built last time to provide some common checking functionality and then we'll use the chained-handler functionality provided by restify to apply the username authentication as part of the routing process. As the routes module has access to the users model (see how we passed it in the code above), we can query it during routing to check the user making the request is on the whitelist. This saves us having to pollute each controller module with the same boilerplate code - in our solution, the controller won't even be called should the client request fail authentication:
var lightsController = require('./controllers/lights');
var groupsController = require('./controllers/groups');
var schedulesController = require('./controllers/schedules');
var configController = require('./controllers/configuration');
var portalController = require('./controllers/portal');

var hasUsernameProperty = function(request) {
 return request && request.params && request.params.hasOwnProperty("username");
}

var validateUser = function(request, response, next) {
 if (hasUsernameProperty(request)) {
  next(); // Authentication successful, route to the real handler...
 }
};

app.post('/api/:username', validateUser, configController.register);
By chaining the validateUser call before the register handler, we can stop processing immediately should a user parameter not be present. Now we can modify our controller (configuration.js) that we created last time to use our new model's interface for client registration:
var bridgeButtonPressed = false;

exports.register = function (request, response) {
    console.log("register - user " + request.params.username);

    if (bridgeButtonPressed) {
        users.addUser(request.params.username);

        response.send(200, [{
            success: { "username": request.params.username }
        }]);
    } else {
        response.send(200, [{
            error: {
                type: 101,
                address: '',
                description: 'link button not pressed'
            }
        }]);
    }
};
Before we go any further, let's just try out the simulator to test this new functionality - there's a little surprise in store - you may have already spotted it in the code above! Run up the server as before:

C:\>node app.js

Then send a request via curl to register a new user:

C:\>curl -is -X POST http://localhost:8080/api/loada
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 77
Date: Tue, 25 Jul 2013 09:45:56 GMT
Connection: keep-alive
[{"error":{"type":101,"address":"","description":"link button not pressed"}}]

As you see, we get an error back - the link button has not been pressed! In the real world, the bridge requires a press of a physical button on top of the unit upto 30 seconds prior to the registration request being received - this is a simple but effective security measure. For now, we'll hard code the simulator to always assume it has been pressed i.e. allow any new user to register at any time. Stop the simulator using CTRL+C and tweak the bridgeButtonPressed variable to true then re-run the simulator - you should now see:

C:\>curl -is -X POST http://localhost:8080/api/loada
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 36
Date: Mon, 25 Jul 2013 12:12:25 GMT
Connection: keep-alive

[{"success":{"username":"loada"}}]

Stopping and then restarting the simulator will show our whitelisted users being loaded at startup:

C:\>node app.js
Hue Bridge Simulator listening at http://0.0.0.0:8080
LoadedUsers = loada

Routing 101...


And that's all there is to the basic routing of requests. We use restify to attach a controller function to our HTTP verb, resource URL and we specify any expected parameters. It will capture the variables from the request and route them to our username authentication handler first and then onto the specified controller function. This function will then query the model (if necessary), package any data (or error) into the expected response format and emit it back to the caller. Pretty much all of the API functions can be implemented with this simple convention leaving us free to focus on implementing the actual bridge functionality.

Next time...


Now the routing concepts are in place, we can begin to add the implementations for the various functions and any supporting functionality we'll need along the way. So, in the next post, we'll add some code for the functions in the lights part of the Hue API. Still need to think of a good name for the project as well!

No comments:

Post a Comment