読者です 読者をやめる 読者になる 読者になる

遥かへのスピードランナー

シリコンバレーでAndroidアプリの開発してます。コンピュータービジョン・3D・アルゴリズム界隈にもたまに出現します。

FirefoxのHTTPプロトコルハンドラを置換してローカルプロキシっぽい動作をさせる

Firefox JavaScript IT

先日の僕のFirefoxアドオン(XPCOM)でHTTPプロクシを実装するの記事の発展系として、piroさんがローカルプロキシっぽいことをローカルプロキシを立てずにやろうとして挫折したことのまとめというすばらしくためになる記事を書かれています。

この記事の中でpiroさんは「特定のURIにアクセスしようとした時だけ、あらかじめ定義しておいたルールに従って別のリソースを返す」ことを実現するために、3つのやり方を提案しています。

  1. ローカルプロキシを実装して、その中でリダイレクトするやり方。
  2. http-on-modify-requestイベントのタイミングでリダイレクトするやり方。
  3. nsIContentPoilcyのshouldLoad()の中でリダイレクトするやり方。

で、結論として2,3で目的を達成するのは難しそう、とのことなのですが、僕がかねてから考えていた4つ目のアイデアがあって、ちょっと試してみたらうまくいきそうな感じがするので途中経過だけど晒してみようかと思います。

4つ目のアイデア-httpプロトコルハンドラを置換する

その4つ目のアイデアというのはFirefoxデフォルトのhttpプロトコルハンドラを置換(上書き)してしまうというものです。

XPCOM コンポーネントの置換 - Days on the Moonにもある通り、Firefoxデフォルトで登録されているXPCOMであっても、同一のコントラクトIDを持つXPCOMを拡張の中で定義することによって、置換することができます。httpのリクエスト処理は「@mozilla.org/network/protocol;1?name=http」を起点にしてほとんどの処理が行われていますが、これを独自のXPCOMに置換してしまえば好きなようにリクエストを加工することができるはずです。

「独自のXPCOMに置換」と言うと、元々のXPCOMの挙動をちょっとだけ変更したい場合でも、全処理をスクラッチで書き直さなければいけないと思いがちですが、Components.classesByIDを使えば、クラスID(UUID)をキーにして置換前のXPCOMを取得することができるので、変更しなくていいところは全て置換前のXPCOMに丸投げしてしまえばよいです。

それを踏まえて、書いてみたのが以下のサンプル。

const Cc   = Components.classes;
const Ci   = Components.interfaces;
const Cr   = Components.results;
const kPROTOCOL_NAME = "my HTTP Protocol Handler";
const kPROTOCOL_CONTRACTID = "@mozilla.org/network/protocol;1?name=http";
const kPROTOCOL_CID = Components.ID("97f7a1c0-4bf6-11de-8a39-0800200c9a66");
//default "@mozilla.org/network/protocol;1?name=http" class id
const ORIGINAL_CLASS = Components.classesByID["{4f47e42e-4d23-4dd3-bfda-eb29255e9ea3}"];


function myHttpProtocolHandler(){
  this._super = ORIGINAL_CLASS.createInstance();
}

myHttpProtocolHandler.prototype = {
  QueryInterface: function(iid) {
    return this._super.QueryInterface(iid);
  },

  allowPort: function(port, scheme) {
    return this._super.QueryInterface(Ci.nsIHttpProtocolHandler).allowPort(port, scheme);
  },

  newURI: function(spec, charset, baseURI) {
    return this._super.QueryInterface(Ci.nsIHttpProtocolHandler).newURI(spec, charset, baseURI);
  },

  newChannel: function(aURI) {
    if (aURI.asciiSpec == "http://www.google.co.jp/") {
      var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);  
      var uri = ioService.newURI("http://www.yahoo.co.jp/", null, null);
      return this._super.QueryInterface(Ci.nsIHttpProtocolHandler).newChannel(uri);
    } else {
      return this._super.QueryInterface(Ci.nsIHttpProtocolHandler).newChannel(aURI);
    }
  },
}


var myHttpProtocolHandlerFactory = {
  createInstance: function (outer, iid) {
    var handler = new myHttpProtocolHandler();
    return handler;
  }
}

var myModule = {
  registerSelf: function (compMgr, fileSpec, location, type) {
    compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
    compMgr.registerFactoryLocation(
      kPROTOCOL_CID,
      kPROTOCOL_NAME,
      kPROTOCOL_CONTRACTID,
      fileSpec, 
      location, 
      type
    );
  },

  getClassObject: function (compMgr, cid, iid) {
    if (!cid.equals(kPROTOCOL_CID))
      throw Cr.NS_ERROR_NO_INTERFACE;

    if (!iid.equals(Ci.nsIFactory))
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
      
    return myHttpProtocolHandlerFactory;
  },

  canUnload: function (compMgr) {
    return true;
  }
}

function NSGetModule (compMgr, fileSpec) {
  return myModule;
}

このようにコントラクトIDは「@mozilla.org/network/protocol;1?name=http」と重複定義して、クラスIDだけを置換前と後で異ならせています。httpリクエスト発行時には置換後のプロトコルハンドラが呼び出されますが、内部で置換前のプロトコルハンドラを呼び出していますので、基本的な処理は変わりません。変わっているのは「function newChannel(aURI)」の部分だけです。

さて、このXPCOMを登録した状態で、http://www.google.co.jp/のリクエストを発行するとYahoo!Japanのページを返します。

スクリーンショットを見てもらえば分かるのですがURL欄はhttp://www.google.co.jp/のままで、内部の処理内でのみURLを書き換えています。どうやらうまく動いているようです。

残課題

とここまで書いておいて何なのですが、実はこのソースにはなぜ正常に動作するのか、分かっていない部分があります。
一つはQueryInterfaceの部分で、QueryInterfaceの部分を置換前のXPCOMに丸投げしてしまっているので、その後の個別の処理も全て置換前XPCOMで行われてもよいはずです。だけど実際にはnewURI,newChannelといったメソッドは置換後のXPCOMのものが呼び出されます。なんでこういう動作をするのかを理解するのはXPCOMの仕組みとFirefoxの内部動作の仕組みをもうちょっと見てみないといけないかなと思ってます。

またもう一つは、このサンプルでも正常に動作しないサイトがいくつかあること。AJAXを多用しているサイトでも基本的には問題なく動くのですが、たとえばGMailにアクセスするとなぜか簡易版HTMLが表示されてしまいます。もしかすると上記の問題と関連しているのかも知れません。

というわけでまだまだ課題がありそうだけど、httpプロトコルハンドラの置換で、かなり柔軟なリクエストフックができるかも知れない、という話でした。これを踏み台にしてさらに発展させてくれる人がいることを願っています(汗)