I am trying to develop a Firefox extension that drops every HTTP request to a certain site and returns a fake response. No request should get through to the original web server, but I want to be able to create a custom response. I tried to intercept the "http-on-modify-request" message, but cancelling the request doesn't seem to work, as I cannot simulate a real response afterwards. Similarly, using an nsITraceableStream instance, I don't seem to be able to really cancel the request. I am out of ideas, can somebody help?
The answer below has been superseded as of Firefox 21, now the nsIHttpChannel.redirectTo() method does the job nicely. You can redirect to a data: URI, something like this will work:
Components.utils.import("resource://gre/modules/Services.jsm");
const Ci = Components.interfaces;
[...]
onModifyRequest: function(channel)
{
if (channel instanceof Ci.nsIHttpChannel && shouldRedirect(channel.URI.spec))
{
let redirectURL = "data:text/html," + encodeURIComponent("<html>Hi there!</html>");
channel.redirectTo(Services.io.newURI(redirectURI, null, null));
}
}
Original answer (outdated)
Each channel has its associated stream listener that gets notified when data is received. All you need to do to fake a response is to get this listener and feed it with wrong data. And nsITraceableChannel is in fact the way to do it. You need to replace the channel's usual listener by your own that won't do anything, after that you can cancel the channel without the listener being notified about it. And then you trigger the listener and give it your own data. Something like this:
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
const Cc = Components.classes;
const Ci = Components.interfaces;
[...]
onModifyRequest: function(channel)
{
if (channel instanceof Ci.nsIHttpChannel && channel instanceof Ci.nsITraceableChannel)
{
// Our own listener for the channel
var fakeListener = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener,
Ci.nsIRequestObserver, Ci.nsIRunnable]),
oldListener: null,
run: function()
{
// Replace old listener by our fake listener
this.oldListener = channel.setNewListener(this);
// Now we can cancel the channel, listener old won't notice
//channel.cancel(Components.results.NS_BINDING_ABORTED);
},
onDataAvailable: function(){},
onStartRequest: function(){},
onStopRequest: function(request, context, status)
{
// Call old listener with our data and set "response" headers
var stream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stream.setData("<html>Hi there!</html>", -1);
this.oldListener.onStartRequest(channel, context);
channel.setResponseHeader("Refresh", "5; url=http://google.com/", false);
this.oldListener.onDataAvailable(channel, context, stream, 0, stream.available());
this.oldListener.onStopRequest(channel, context, Components.results.NS_OK);
}
}
// We cannot replace the listener right now, see
// https://bugzilla.mozilla.org/show_bug.cgi?id=646370.
// Do it asynchronously instead.
var threadManager = Cc["@mozilla.org/thread-manager;1"]
.getService(Ci.nsIThreadManager);
threadManager.currentThread.dispatch(fakeListener, Ci.nsIEventTarget.DISPATCH_NORMAL);
}
}
The problem with this code is still that the page shows up blank if the channel is canceled (so I commented that line) - it seems that the listener still looks at the channel and notices that it is canceled.