Search code examples
javascriptfirefox-addonxpcomxmlhttprequest

Create Firefox Addon to Watch and modify XHR requests & reponses


Update: I guess the subject gave a wrong notion that I'm looking for an existing addon. This is a custom problem and I do NOT want an existing solution.
I wish to WRITE (or more appropriately, modify and existing) Addon.

Here's my requirement:

  • I want my addon to work for a particular site only
  • The data on the pages are encoded using a 2 way hash
  • A good deal of info is loaded by XHR requests, and sometimes displayed in animated bubbles etc.
  • The current version of my addon parses the page via XPath expressions, decodes the data, and replaces them

  • The issue comes in with those bubblified boxes that are displayed on mouse-over event

  • Thus, I realized that it might be a good idea to create an XHR bridge that could listen to all the data and decode/encode on the fly
  • After a couple of searches, I came across nsITraceableInterface[1][2][3]

Just wanted to know if I am on the correct path. If "yes", then kindly provide any extra pointers and suggestions that may be appropriate; and if "No", then.. well, please help with correct pointers :)

Thanks,
Bipin.

[1]. https://developer.mozilla.org/en/NsITraceableChannel
[2]. http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
[3]. http://www.ashita.org/howto-xhr-listening-by-a-firefox-addon/


Solution

  • nsITraceableChannel is indeed the way to go here. the blog posts by Jan Odvarko (softwareishard.com) and myself (ashita.org) show how to do this. You may also want to see http://www.ashita.org/implementing-an-xpcom-firefox-interface-and-creating-observers/, however it isn't really necessary to do this in an XPCOM component.

    The steps are basically:

    1. Create Object prototype implementing nsITraceableChannel; and create observer to listen to http-on-modify-request and http-on-examine-response
    2. register observer
    3. observer listening to the two request types adds our nsITraceableChannel object into the chain of listeners and make sure that our nsITC knows who is next in the chain
    4. nsITC object provides three callbacks and each will be called at the appropriate stage: onStartRequest, onDataAvailable, and onStopRequest
    5. in each of the callbacks above, our nsITC object must pass on the data to the next item in the chain

    Below is actual code from a site-specific add-on I wrote that behaves very similarly to yours from what I can tell.

    function TracingListener() {
        //this.receivedData = [];
    }
    
    TracingListener.prototype =
    {
        originalListener: null,
        receivedData: null,   // 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");
            binaryInputStream.setInputStream(inputStream);
            storageStream.init(8192, count, null);
    
            var binaryOutputStream = CCIN("@mozilla.org/binaryoutputstream;1",
                    "nsIBinaryOutputStream");
    
            binaryOutputStream.setOutputStream(storageStream.getOutputStream(0));
    
            // Copy received data as they come.
            var data = binaryInputStream.readBytes(count);
            //var data = inputStream.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.receivedData = [];
            this.originalListener.onStartRequest(request, context);
        },
    
        onStopRequest: function(request, context, statusCode)
        {
            try 
            {
                request.QueryInterface(Ci.nsIHttpChannel);
    
                if (request.originalURI && piratequesting.baseURL == request.originalURI.prePath && request.originalURI.path.indexOf("/index.php?ajax=") == 0) 
                {
    
                    var data = null;
                    if (request.requestMethod.toLowerCase() == "post") 
                    {
                        var postText = this.readPostTextFromRequest(request, context);
                        if (postText) 
                            data = ((String)(postText)).parseQuery();
    
                    }
                    var date = Date.parse(request.getResponseHeader("Date"));
                    var responseSource = this.receivedData.join('');
    
                    //fix leading spaces bug
                    responseSource = responseSource.replace(/^\s+(\S[\s\S]+)/, "$1");
    
                    piratequesting.ProcessRawResponse(request.originalURI.spec, responseSource, date, data);
                }
            } 
            catch (e) 
            {
                dumpError(e);
            }
            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;
        },
        readPostTextFromRequest : function(request, context) {
            try
            {
                var is = request.QueryInterface(Ci.nsIUploadChannel).uploadStream;
                if (is)
                {
                    var ss = is.QueryInterface(Ci.nsISeekableStream);
                    var prevOffset;
                    if (ss)
                    {
                        prevOffset = ss.tell();
                        ss.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
                    }
    
                    // Read data from the stream..
                    var charset = "UTF-8";
                    var text = this.readFromStream(is, charset, true);
    
                    // Seek locks the file so, seek to the beginning only if necko hasn't read it yet,
                    // since necko doesn't seek to 0 before reading (at lest not till 459384 is fixed).
                    if (ss && prevOffset == 0) 
                        ss.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
    
                    return text;
                }
                else {
                    dump("Failed to Query Interface for upload stream.\n");
                }
            }
            catch(exc)
            {
                dumpError(exc);
            }
    
            return null;
        },
        readFromStream : function(stream, charset, noClose) {
    
            var sis = CCSV("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream");
            sis.setInputStream(stream);
    
            var segments = [];
            for (var count = stream.available(); count; count = stream.available())
                segments.push(sis.readBytes(count));
    
            if (!noClose)
                sis.close();
    
            var text = segments.join("");
            return text;
        }
    
    }
    
    
    hRO = {
    
        observe: function(request, aTopic, aData){
            try {
                if (typeof Cc == "undefined") {
                    var Cc = Components.classes;
                }
                if (typeof Ci == "undefined") {
                    var Ci = Components.interfaces;
                }
                if (aTopic == "http-on-examine-response") {
                    request.QueryInterface(Ci.nsIHttpChannel);
    
                    if (request.originalURI && piratequesting.baseURL == request.originalURI.prePath && request.originalURI.path.indexOf("/index.php?ajax=") == 0) {
                        var newListener = new TracingListener();
                        request.QueryInterface(Ci.nsITraceableChannel);
                        newListener.originalListener = request.setNewListener(newListener);
                    }
                } 
            } catch (e) {
                dump("\nhRO error: \n\tMessage: " + e.message + "\n\tFile: " + e.fileName + "  line: " + e.lineNumber + "\n");
            }
        },
    
        QueryInterface: function(aIID){
            if (typeof Cc == "undefined") {
                var Cc = Components.classes;
            }
            if (typeof Ci == "undefined") {
                var Ci = Components.interfaces;
            }
            if (aIID.equals(Ci.nsIObserver) ||
            aIID.equals(Ci.nsISupports)) {
                return this;
            }
    
            throw Components.results.NS_NOINTERFACE;
    
        },
    };
    
    
    var observerService = Cc["@mozilla.org/observer-service;1"]
        .getService(Ci.nsIObserverService);
    
    observerService.addObserver(hRO,
        "http-on-examine-response", false);
    

    In the above code, originalListener is the listener we are inserting ourselves before in the chain. It is vital that you keep that info when creating the Tracing Listener and pass on the data in all three callbacks. Otherwise nothing will work (pages won't even load. Firefox itself is last in the chain).

    Note: there are some functions called in the code above which are part of the piratequesting add-on, e.g.: parseQuery() and dumpError()