I have been waiting for a long time to write about this. Since Bug 430155 (new nsHttpChannel interface to allow examination of HTTP data before it is passed to the channel's creator) is now fixed, it's possible to intercept HTTP traffic from within a Firefox extension!

This fix should be part of Firefox 3.0.4.

This possibility is crucial for Firebug (and also Adblock Plus and Firekeeper can benefit from it) as one of its features is showing a response of any HTTP request made by a page. Until now, Firebug has been using Firefox cache and XHR monitoring to retrieve the response and display it to the user. While this worked quite fine for XHR (it's not that hard to monitor XHR responses) there was a big issue with monitoring all the other network requests. The most visible problem was probably submit of an HTTP form using POST method (known as double-load problem).

In some cases, the HTTP response isn't cached (and don't forget that Firefox cache can be disabled entirely) and Firebug had to fire a second request in order to get the response again and show it to the user. Of course, this was a big problem for every server that executed any (e.g. database) related actions (these have been executed twice). Uff...

Finally, I could implement new and reliable cache within Firebug 1.3 based on nsITraceableChannel. So, Firebug can use it anytime the response-source is needed without an additional request, e.g. if the user wants to see a network response in Firebug's Net panel...

Check it out, I am recommending latest Firebug 1.3 from SVN and till the Firefox 3.0.3 isn't available you can download latest trunk build from here. Any comments truly appreciated!

For those who are more interested, I have written an example that demonstrates how the nsITraceableChannel can be used.

How to intercept HTTP traffic

The main purpose of the nsITraceableChannel interface is to register a stream listener (nsIStreamListener) into an HTTP channel and monitor all data in it.
In order to register the listener within every incoming HTTP channel we also need to observe http-on-examine-response event. This event is fired whenever a response is received from the server, but before any data are available.

This is how the observer can be registered.

const Cc = Components.classes;
const Ci = Components.interfaces;

var observerService = Cc["@mozilla.org/observer-service;1"]
    .getService(Ci.nsIObserverService);

observerService.addObserver(httpRequestObserver,
    "http-on-examine-response", false);

Of course, we should make sure it's also properly unregistered.

observerService.removeObserver(httpRequestObserver,
    "http-on-examine-response");

Implementation of the request-observer should be as follows:

var httpRequestObserver =
{
    observe: function(aSubject, aTopic, aData)
    {
        if (aTopic == "http-on-examine-response")
        {
        }
    },

    QueryInterface : function (aIID)
    {
        if (aIID.equals(Ci.nsIObserver) ||
            aIID.equals(Ci.nsISupports))
        {
            return this;
        }

        throw Components.results.NS_NOINTERFACE;

    }
});

See more info about http-on-modify-request and http-on-examine-response here.

The next step is to implement the stream listener. The listener is realized as a wrapper of the original listener. So, don't forget to call it in each method. If multiple extensions register a listener using the nsITraceableChannel, a chain of listeners is created.

function TracingListener() {
}

TracingListener.prototype =
{
    originalListener: null,

    onDataAvailable: function(request, context, inputStream, offset, count) {
        this.originalListener.onDataAvailable(request, context, inputStream, offset, count);
    },

    onStartRequest: function(request, context) {
        this.originalListener.onStartRequest(request, context);
    },

    onStopRequest: function(request, context, statusCode) {
        this.originalListener.onStopRequest(request, context, statusCode);
    },

    QueryInterface: function (aIID) {
        if (aIID.equals(Ci.nsIStreamListener) ||
            aIID.equals(Ci.nsISupports)) {
            return this;
        }
        throw Components.results.NS_NOINTERFACE;
    }
}

The registration is made within the httpRequestObserver.observe method. Just set the listener using setNewListener method and remember the original listener.

observe: function(aSubject, aTopic, aData)
{
    if (aTopic == "http-on-examine-response") {
        var newListener = new TracingListener();
        aSubject.QueryInterface(Ci.nsITraceableChannel);
        newListener.originalListener = aSubject.setNewListener(newListener);
    }
}

That's it!

Here is yet another implementation of the TracingListener that copies all the incoming data.

// Helper function for XPCOM instanciation (from Firebug)
function CCIN(cName, ifaceName) {
    return Cc[cName].createInstance(Ci[ifaceName]);
}

// Copy response listener implementation.
function TracingListener() {
}

TracingListener.prototype =
{
    originalListener: null,
    receivedData: [],   // array for incoming data.

    onDataAvailable: function(request, context, inputStream, offset, count)
    {
        var binaryInputStream = CCIN("@mozilla.org/binaryinputstream;1",
                "nsIBinaryInputStream");
        var storageStream = CCIN("@mozilla.org/storagestream;1", "nsIStorageStream");
        var binaryOutputStream = CCIN("@mozilla.org/binaryoutputstream;1",
                "nsIBinaryOutputStream");
           
        binaryInputStream.setInputStream(inputStream);
        storageStream.init(8192, count, null);
        binaryOutputStream.setOutputStream(storageStream.getOutputStream(0));

        // Copy received data as they come.
        var data = binaryInputStream.readBytes(count);
        this.receivedData.push(data);

        binaryOutputStream.writeBytes(data, count);

        this.originalListener.onDataAvailable(request, context,
            storageStream.newInputStream(0), offset, count);
    },

    onStartRequest: function(request, context) {
        this.originalListener.onStartRequest(request, context);
    },

    onStopRequest: function(request, context, statusCode)
    {
        // Get entire response
        var responseSource = this.receivedData.join();
        this.originalListener.onStopRequest(request, context, statusCode);
    },

    QueryInterface: function (aIID) {
        if (aIID.equals(Ci.nsIStreamListener) ||
            aIID.equals(Ci.nsISupports)) {
            return this;
        }
        throw Components.results.NS_NOINTERFACE;
    }
}


Rss Commenti

18 Comments

  1. Thanks for the examples. I already looked at an implementation with a storage stream in the bug - but that seems very inefficient (especially the fact that you have to create a new storage stream for each chunk of data). I wonder whether the same can be done is a more straightforward way. Unfortunately, you cannot implement nsIInputStream in JavaScript.

    #1 Wladimir Palant
  2. " newListener.listener = request.setNewListener(newListener);"

    Wouldn't that be:
    " newListener.originalListener = request.setNewListener(newListener);"
    ?

    #2 Asrail
  3. Yep, you right. Fixed, thanks!
    Honza

    #3 admin
  4. So does this fix for the bug 430115 mean that I can not only gain access to the HTTP response before it is parsed (e.g. JavaScript is executed) but also modify the text of the response (e.g. JavaScript content) so that the first time the Gecko engine parses / executes the JavaScript, it will be my modified version of the JavaScript instead of the original?

    I have been looking for a solution like this for some time as it would help me enable web sites that have been specifically targeted for IE to prevent them from running in Mozilla.

    Your answer is kindly appreciated...

    #4 James
  5. @James: yes, this is possible. Just past whatever you want to the original stream listener within the onDataAvailable method.
    Honza

    #5 admin
  6. Thanks for your answer. One other quick question. Will this allow me to get at async/ajax requests/responses?

    #6 James
  7. @James: yes, this interface allows to intercept all incoming responses including XHRs.
    Honza

    #7 admin
  8. Will firebug ever be able to time DNS resolution separately from HTTP requests? I've been looking through the Firefox source for a good place to get at this info and it doesn't look like you can by using any existing services.

    #8 Matt
  9. In the above example, there is a piece of code.
    request.QueryInterface(Ci.nsITraceableChannel);

    I presume 'request' is not an implicit object available in the observe : function(aSubject, aTopic, aData).

    Where was a reference to request object established in the observe() ?

    Do I miss something here ? As you could observe I am not too clear about this statement.
    request.QueryInterface(Ci.nsITraceableChannel);

    #9 Sunil
  10. @Sunil: fixed, thanks! When http-on-examine-response event is fired, the request object is passed into the observe method as a subject.
    Honza

    #10 admin
  11. Hi, I check this solution and I changhe this line in:

    aSubject.QueryInterface(Ci.nsITraceableChannel);
    in
    aSubject.QueryInterface(Components.interfaces.nsITraceableChannel);

    Firefox 3.0.1 says:

    Errore: Could not convert JavaScript argument arg 0 [nsISupports.QueryInterface] = NS_ERROR_XPC_BAD_CONVERT_JS

    Where is my error?

    tnx, cnt00

    #11 cnt00
  12. The nsITraceableChannel interface is quite new. What version of Firefox do you use? Try Firefox 3.0.4pre, http://ftp.mozilla.org/pub/mozilla.org/firefox/nightly/latest-mozilla1.9.0/ and let me know.
    Honza

    #12 admin
  13. Sorry i use FF 3.0.1
    Now i update my FF to 3.0.3

    The error now is:
    Error: aSubject.setNewListener is not a function

    The line is:
    newListener.originalListener = aSubject.setNewListener(newListener);

    I don't understand because i see this meth in
    http://mxr.mozilla.org/firefox/source/netwerk/base/public/nsITraceableChannel.idl

    I have to use 3.0.4?

    Tnx a lot!

    #13 cnt00
  14. Yes. The interface should be available since 3.0.4 (it should have been part of 3.0.3, but the patch didn't make it).
    Honza

    #14 admin
  15. It works with 3.0.4pre!
    Tnx!

    #15 cnt00
  16. Can we use this to get data for "http-on-modify-request"?

    #16 Yogi
  17. @Yogi: if by "data" you mean an HTTP response then yes.

    #17 admin
  18. Hi,
    i need to modify ALL the http body when is completed.
    Example in you code: i need to modify "responseSource" and not "data" inside method onDataAvailable.
    It's possible? Sorry and tnx

    #18 cnt00

Leave a comment