8

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?

Niklas B.
  • 92,950
  • 18
  • 194
  • 224
  • What is this extension supposed to be used for? – Amir Raminfar Aug 28 '11 at 17:31
  • Maybe you want to have a look how LeechBlock blocks the requests: https://addons.mozilla.org/en-US/firefox/addon/leechblock/ – Felix Kling Aug 28 '11 at 17:52
  • @Felix: I know how to block requests, but if I do it the usual way (reacting to the "http-on-modify-request" message), I can not fake a response. – Niklas B. Aug 28 '11 at 18:24
  • @Amir: We (an IT security company I work for) want to use it for demonstration purposes to show how easy it is to manipulate SSL-secured connections when you have access to the client side (browser). – Niklas B. Aug 28 '11 at 18:37
  • 1
    @Niklas: Why not just use the Fiddler (www.fiddler2.com) AutoResponder to do this? You can then do your demo in ALL browsers. – EricLaw Aug 29 '11 at 00:13
  • @EricLaw: I am having a hard time imagining that Fiddler can manipulate HTTPS responses without at least triggering a certificate warning. **Edit**: Right: http://www.fiddler2.com/fiddler/help/httpsdecryption.asp – Wladimir Palant Aug 29 '11 at 06:14
  • @EricLaw: We do it right now using a proxy and a trusted company CA in the browser. But it is even nicer to have the original EV-trust shown in the location bar. – Niklas B. Aug 29 '11 at 11:59

1 Answers1

9

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.

Wladimir Palant
  • 56,865
  • 12
  • 98
  • 126
  • Thanks Wladimir, I'll check it out! – Niklas B. Aug 29 '11 at 12:04
  • Hello Wladimir, I tried your code in Firefox 6, and FiFo segfaults on `oldListener.onStartRequest(channel, oldContext);` I'll try FiFo 3.5 now just to be sure. I actually used a very similar approach with a null-listener, but in onExamineResponse, where I didn't seem to be able to cancel the request and to set any response headers. – Niklas B. Aug 29 '11 at 12:48
  • @Niklas: That's probably because `oldContext` hasn't been set at this point so that you are calling it with the wrong context - I guess that the channel is canceled asynchronously which means that `onStopRequest` is called too late. Moving all the code faking a request (`onStartRequest` & Co.) into `onStopRequest` of your fake listener should fix this. – Wladimir Palant Aug 29 '11 at 13:02
  • I had this thought already and tried [this](http://pastebin.com/kSdZpKfP) code, but none of the alerts in fakeListener is ever called. The `alert("Registering fake listener...")`, however, is called. **Edit:** The fake listener is not even called when I do _not_ cancel the request. – Niklas B. Aug 29 '11 at 13:55
  • @Niklas: Calling `alert()` in the networking code is a bad idea and might not work for a reason (it produces a modal dialog). You should use `Components.utils.reportError()` or `window.dump()` which produce more reliable results. – Wladimir Palant Aug 29 '11 at 13:58
  • Okay I didn't know that. Now I use `Components.utils.reportError()`, with the same results. It seems as though the `setNewListener` wouldn't have any effect at all... is it maybe possible that this only works in reaction to `http-on-examine-response`? All the example code I've seen so far involving `setNewListener` actually calls it in `onExamineResponse`. – Niklas B. Aug 29 '11 at 14:04
  • @Niklas: Forgot about that: [bug 646370](https://bugzilla.mozilla.org/show_bug.cgi?id=646370) :-( – Wladimir Palant Aug 29 '11 at 14:07
  • too bad :( do you maybe have an idea how I could manipulate the response headers in http-on-examine-response? The approach I had before your answer was to change request to the `TRACE` method, which has no effect on the server side. I can then manipulate the response body in `onExmineResponse`, but it seems that I cannot call `setResponseHeader` (throws `NS_ERROR_ILLEGAL_VALUE`) – Niklas B. Aug 29 '11 at 14:13
  • @Niklas: I fixed up the code as much as possible, see edited version of my post. It works for me, at least as long as I don't cancel the original request and don't try to set response headers. – Wladimir Palant Aug 29 '11 at 14:28
  • so no way to set response headers? This is essential for me. And I don't just need to set `channel.contentType`. – Niklas B. Aug 29 '11 at 14:32
  • @Niklas: See revised version of the post again. It will only reject the `Content-Type` header, probably because the channel already has one at this point. Other headers can be set however. – Wladimir Palant Aug 29 '11 at 14:37
  • sorry for this long discussion... What you say is correct, calling `setResponseHeader` for example with `"Set-Cookie"` or `"Refresh"` does not throw an exception, but Firefox does not seem to respect headers set that way. It does not refresh and it does not set cookies, at least for me. If it does for you, can you tell me which version of Firefox you use? I use 6.0 on Linux x86_64. **Edit:** I also really want to thank you for the time you have put into this issue! – Niklas B. Aug 29 '11 at 14:51
  • @Niklas: I actually tried `Refresh` in Firefox 9.0a1, it is works (this Firefox version blocks the redirect however). You might want to change the third parameter to `setResponseHeader` (`aMerge`) to `true`, it is definitely required if other cookie headers are present. – Wladimir Palant Aug 29 '11 at 14:54
  • This proves to be a very tough issue. I downloaded 9.0a1, but neither the Refresh (using you literal code), nor the Set-Cookie works. I also don't know if the blocking you mentioned is invisible? If yes, how can I verify that the refresh occurs but is blocked? – Niklas B. Aug 29 '11 at 15:04
  • @Niklas: The blocking results in a notification bar popping up. Unfortunately, I no longer have time to verify whether `Set-Cookie` headers work for me (or try more tweaks of this code). – Wladimir Palant Aug 29 '11 at 15:07
  • I understand completely. I more and more get the impression that the Javascript extension approach is a blind end. Maybe I should rather hack some of the CPP source to get a custom Firefox build that integrates my redirection code or at least has [646370](https://bugzilla.mozilla.org/show_bug.cgi?id=646370) fixed. Thank you very much for your excellent help. – Niklas B. Aug 29 '11 at 15:10
  • @Niklas: You might want to try setting headers earlier, maybe in `onStartRequest`. As to [bug 646370](https://bugzilla.mozilla.org/show_bug.cgi?id=646370) - I don't think that it is an issue here, the work-around works just fine. – Wladimir Palant Aug 29 '11 at 15:15
  • Actually I was simply too stupid to name variables right. The parameter to onStopRequest is called request, but I called channel.setResponseHeader. Maybe you can update your code so other users will not get confused :) **EDIT:** I am however not sure why this is not the same instance.. Well at least it works now. – Niklas B. Aug 29 '11 at 15:18
  • @Niklas: Given that `onStopRequest()` is a closure defined inside the `onModifyRequest()` function, it is perfectly fine to use the `channel` variable from the outer function (it is even required for the `run()` method). But maybe you have a different setup. – Wladimir Palant Aug 29 '11 at 15:21
  • Yes, you are right. The problem was that I needed to set the headers in `onStartRequest`. – Niklas B. Aug 29 '11 at 15:29
  • @Niklas: In your case there probably was a redirect - so the channel in `onStopRequest` wasn't the same as the one you attached your listener to. I should edit my code to consider that case. – Wladimir Palant Aug 29 '11 at 19:36