af83

HTTP all the things

Recently, I've heard about the browserver project, that turns your browser in a pseudo web-server by using web-sockets. While the form is surely new, I found the idea quite familiar. Indeed, there has been quite a few attempts at embedding a web server in every user's browser: for example this add-on for the (discontinued) Opera Unite promised to revolutionize the way you shared files with friends by embedding a web server in your browser.

Furthermore, my recent efforts to control Rdio's web app from the command-line have led me to explore Firefox's add-on SDK in order to… have an extension start its own web server. The good thing is that this is done from the comfort of JavaScript, by abusing the httpd module normally available to run unit tests. Sure, I'm not really writing unit tests here, but it couldn't just sit here unused, could it?

Abusing Mozilla's add-on SDK for fun and profit

While Firefox's documentation on how to create an add-on and get it running is excellent (so this won't be covered here), it is quite sparse when it comes to abusing its unit-testing modules. The httpd module is available for us add-on writers: it “provides an HTTP server written in JavaScript for the Mozilla platform, which can be used in unit tests.” Let's use that for more creative things…

The first example from Mozilla, pasted here, is about serving files from a directory:

var {startServerAsync} = require("httpd");
var srv = startServerAsync(port, basePath);
require("unload").when(function cleanup() {
  srv.stop(function() { // you should continue execution from this point.
  })
});

OK, that was sort of instructive. But don't despair yet, you can also do your own thing:

var {nsHttpServer} = require("httpd");
var srv = new nsHttpServer();

And that's about it for the add-on's documentation.

Of course, the required module's source is available and quite interesting if you care to have a look at it: the httpd.js file is bundled with the add-on SDK, and also available on Github. If you have even more time on your hands you can also read the lower-level interface definitions. While these are a little arid, and not entirely reproduced in JavaScript, it is a very instructive read on which behavior you should expect from the underlying implementation.

Getting to know your browser's httpd

By following the latest example, you should have created an instance of nsHttpServer, which you can now start and stop with the following:

// I won't repeat these two lines anymore:
var {nsHttpServer} = require("httpd");
var srv = new nsHttpServer();

// Start listening on port 8000
srv.start(8000);

// Stop it.
srv.stop(function() { console.log("Stopped."); });

There are a handful of other (public) methods, but I'll only cover a couple here that can be used to actually serve content.

registerPathHandler

Match a path (a String) to a handler:

srv.registerPathHandler("/", function(request, response) {
  response.write("Hello world!\n");
});

From there you can already query your awesome server with whatever HTTP verb, it does not care:

$ curl localhost:8000
Hello world!
$ curl -XPUT localhost:8000
Hello world!

registerErrorHandler

This method allows you to catch generic HTTP errors by matching a status-code. For example: throwing a generic exception in a path-handler will trigger the callback registered with the code 500.

Note that setting the appropriate HTTP headers to indicate the status in the response is up to you.

srv.registerPathHandler("/bim", function(request, response) {
  throw "error";
});

srv.registerPrefixHandler(500, function(request, response) {
  // Set response headers
  response.setStatusLine(request.httpVersion, 500, "KO");

  // Complete response body
  response.write("BIM!\n");
});

Requests and responses

When registering a path — or error — handler, the callback receives two objects, namely Request and Response, in that order.

  • With no big surprises, Request provides information on the incoming request: host, port, queryString, httpVersion, …
  • Response allows you to write a response to the world, with two main methods:
    • write accepts "data", e.g.: write("whatever") ;
    • setStatusLine sets the HTTP status-line header, e.g.: setStatusLine("1.1", 200, "OK")

Wrapping up

I'm sure not every Firefox user wants to have add-ons start web servers in their back, but this little hack is an example of the powerful interfaces that Mozilla provides to add-on developers, directly in JavaScript. Hopefully this gave you some ideas about what you could do within a browser when you're done making the next coolest Backbone app ever. :)

As a side-note, if you're interested in hacking around this, maybe you'd like to have a nicer interface than registerPathHandler. I started a very simple wrapper that provides a somewhat nicer interface. Maybe reading about this will inspire people with some talent for JavaScript to write a cleaner one than I did.

With enough spare time, I'm sure one could port existing Javascript web frameworks to the add-on SDK…

blog comments powered by Disqus