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

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

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

先日の僕の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プロトコルハンドラの置換で、かなり柔軟なリクエストフックができるかも知れない、という話でした。これを踏み台にしてさらに発展させてくれる人がいることを願っています(汗)

FirefoxでProxy

inspired by http://moz-addon.g.hatena.ne.jp/ZIGOROu/20090518/1242640418

とりあえず、プロクシ設定して(色々不安定ながら)動くところまで。。
http://coderepos.org/share/browser/platform/firefox/Mogwai/trunk/src/components/MobileGateway.js?rev=33576

以下TODO。

  • Clientからのリクエストデータ読み込みをreadLineで行って1行まるごと解析しているが、Windows標準のtelnetのように1文字ずつ送信された場合などに対応できないので読み込んだデータからCRLFを検索するようにする。そもそも今はCRLF以外の改行コードでも、1行とみなされるが、HTTPの仕様上CRLFのみに対応すべき。
  • HTTPリクエストが相対パスでなされた場合の対応。
  • POSTリクエストの場合、Content-length分だけメッセージボディを読み込むようにする。
  • GETとPOSTくらいしか考慮していないので、他のメソッドにも対応する。(というか、メソッド行だけ読み込んで、あとはサーバー側に丸々コピーすればよいのか?)
  • ソケットの接続が切れた場合の対応とかが考慮されてない。
  • keep-aliveとか。
  • SSLとか。
  • マルチスレッド。
  • 不正なリクエストへの対応。エラーハンドリング。
  • ストリーム部分がいろんなInputStreamに切り替えていて見苦しいので何とかしたい。
  • GMailとかが正常に動かない。
  • dumpからの卒業。

タブごとに端末選択可能なFireMobileSimulatorベータ版公開と人柱募集

「タブごとの選択機能」をβ版にて提供しておりましたが、2011.5.6にリリースした本家FireMobileSimulatorのバージョン1.2.0から、この機能がマージされております。お手数ですが、本家サイトより最新版のFireMobileSimulatorをダウンロードして下さい。

    • -

IRCには書き込んだのですが、FireMobileSimulatorで要望の多かった「タブごとに端末選択可能」を実装した新バージョンを作成したので公開します。

タブごとのHTTPリクエスト判別に結構特殊なことをやっているのと、インターフェース周りで使いづらい点があるかも知れないので、今回はベータ版として公開しています。人柱になってもいいよ、という方だけインストールしてください。
実際使ってみると、↓こんな感じで画面下部のステータスバーに選択している端末が表示されます。

不具合や要望などあったら、このエントリのコメント欄か、IRCチャネルfms-devel@freenodeに書き込んでいただけると嬉しいです。

タブごとのHTTPリクエストの識別には、

に書いたような処理をやってます。

あと、タブ分割の拡張機能Split Browserとの併用もできるようにしようと頑張ってみたのですが、今回はちょっと無理でした。タブ分割すると、タブごとに保存した内容が失われてしまうようなので、この辺はSplit Browser側で分割したタブごとにsessionStoreみたいな仕組みが使えると嬉しいな、と言ってみたりして。(自分が見つけられてないだけか?)

はてなブックマークFirefox拡張を入れてみた

はてなブックマーク Firefox 拡張のベータテストを開始します】
http://hatena.g.hatena.ne.jp/hatenabookmark/20090402/firefox_beta

Firefoxローカルデータと同期する機能はよいですね。ローカルのブックマークと連携したりとかするといろいろ未来が広がりそうです。

個人的にはあと、人気エントリ・注目エントリに関する機能があってもよいかなあと思います。関連しそうな注目エントリをサイドバーに表示させてあげるとか。

ちなみに、ちゃんと公開はされていないようですが今開いているページのブックマーク数は、
http://b.hatena.ne.jp/entry.count?url=http%3A//d.hatena.ne.jp/
APIを利用して取得されていますね。

FireMobileSimulatorのIRCチャンネル作りました

FireMobileSimulatorの開発者を広げていくにはどうすればいいのか?という疑問をぶつけたところ、id:ZIGOROuさんからIRCチャンネルを作ってはどうかという案を頂いたので早速作ってみました。

#fms-devel@freenode

になります。
FireMobileSimulatorの開発に興味を持っている方、質問などお気軽にお願いします。
(その前にコードをリファクタリングした方がいいとは思いつつも・・・)

Firefoxアドオンでちょっとコアにタブを扱う

Firefoxのアドオンで、ちょっとコアにタブを扱う処理のメモ。

http-on-modify-requestなどのトピック発生時に、HTTPリクエスト発生元のタブを取得するサンプルコード

getTabFromHttpChannelでtry-catchしている部分は、リクエスト元がDOMWindowじゃない場合にExceptionが発生するのでキャッチしているが、あんまり綺麗じゃないので他にうまいやり方はないだろうか。

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

//myHTTPListnerはhttp-on-modify-requestなどのトピックのObserverとして登録するオブジェクト
//Observer登録部分のソースは省略
function myHTTPListener() {};
myHTTPListener.prototype = {
  observe : function (subject, topic, data) {
    var httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
    var tab = getTabFromHttpChannel(httpChannel);
    if (tab) {
      //リクエスト元のタブに対する処理
    }
  }
};

function getTabFromHttpChannel (httpChannel) {
  var tab = null;
  if (httpChannel.notificationCallbacks) {
    var interfaceRequestor = httpChannel.notificationCallbacks
        .QueryInterface(Ci.nsIInterfaceRequestor);
    try {
      var targetDoc = interfaceRequestor.getInterface(Ci.nsIDOMWindow).document;
    } catch (e) {
      return;
    }
    var webNav = httpChannel.notificationCallbacks.getInterface(Ci.nsIWebNavigation);
    var mainWindow = webNav.QueryInterface(Ci.nsIDocShellTreeItem)
        .rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
        .getInterface(Ci.nsIDOMWindow);

    var gBrowser = mainWindow.getBrowser();
    var targetBrowserIndex = gBrowser.getBrowserIndexForDocument(targetDoc);
    if (targetBrowserIndex != -1) {
      tab = gBrowser.tabContainer.childNodes[targetBrowserIndex];
    }
  }
  return tab;
};
タブごとに値を保存・取得するサンプルコード

タブごとに任意の値を保存するには、単に独自プロパティを1つ追加してもよいが、sessionStoreを使うとFireFoxを再起動してタブ復元しても保存される。詳細はこのへん

var tab = gBrowser.selectedTab; 
var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);

//タブに任意の値を保存
ss.setTabValue(tab, "your-attribute", "your-value");

//タブに保存した値を取得
var yourValue = ss.getTabValue(tab, "your-attribute");

QueryInterfaceとgetInterfaceの違い

Firefoxアドオンでちょっとコアにタブを扱う」を書いてみて、インターフェースを取得するのに、QueryInterface(Ci.nsIXXXXX)としている箇所と、QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIXXXXX)としている箇所の違いが自分でもよく理解できなかったので、いろいろ調べてみた。

まず、MDCには以下の説明がある。
getInterface - MDCより

getInterface is very similar to QueryInterface. The main difference is that interfaces returned from getInterface are not required to provide a way back to the object implementing nsIInterfaceRequestor. The semantics of QueryInterface dictate that given an interface A that you QueryInterface on to get to interface B, you must be able to QueryInterface on B to get back to A. nsIInterfaceRequestor, however, allows you to obtain an interface C from A that may (or most likely) will not have the ability to get back to A.

QueryInterfaceは可逆だけど、getInterfaceは不可逆であるという説明だが、いまいち良く分からない。そこで、真実はソースコードのみ、ということでFirefoxソースコードを調べてみた。
結果として、QueryInterfaceとgetInterfaceはプログラム上役割が決まっているというものではなく、実装に依ってその役割が異なってくる。しかし、ざっとソースを眺めたところのそれぞれのメソッドの役割はおおよそ固定化されており、以下の通りとなっていることが分かった。

  • QueryInterfaceはそのオブジェクト自身に、指定されたインターフェースを持っているかどうかを問い合わせるメソッドであり、返却値はキャストされた自分自身である(ことが多い)。
  • getInterfaceはそのオブジェクト自身に、指定されたインターフェースを持っている必要はなく、何らかの方法で(たとえばメンバ変数を返すなど)指定されたインターフェースを持つオブジェクトを返すメソッドである。従って返却値は自分自身ではないことが多い。

nsDocShellを例にあげて見ていくと、まずQueryInterfaceの実装は、以下の通り。
nsDocShell.cppより

NS_INTERFACE_MAP_BEGIN(nsDocShell)
    NS_INTERFACE_MAP_ENTRY(nsIDocShell)
    NS_INTERFACE_MAP_ENTRY(nsIDocShellTreeItem)
    NS_INTERFACE_MAP_ENTRY(nsIDocShellTreeNode)
    NS_INTERFACE_MAP_ENTRY(nsIDocShellHistory)
    NS_INTERFACE_MAP_ENTRY(nsIWebNavigation)
    NS_INTERFACE_MAP_ENTRY(nsIBaseWindow)
    NS_INTERFACE_MAP_ENTRY(nsIScrollable)
    NS_INTERFACE_MAP_ENTRY(nsITextScroll)
    NS_INTERFACE_MAP_ENTRY(nsIDocCharset)
    NS_INTERFACE_MAP_ENTRY(nsIScriptGlobalObjectOwner)
    NS_INTERFACE_MAP_ENTRY(nsIRefreshURI)
    NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener)
    NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
    NS_INTERFACE_MAP_ENTRY(nsIContentViewerContainer)
    NS_INTERFACE_MAP_ENTRY(nsIEditorDocShell)
    NS_INTERFACE_MAP_ENTRY(nsIWebPageDescriptor)
    NS_INTERFACE_MAP_ENTRY(nsIAuthPromptProvider)
    NS_INTERFACE_MAP_ENTRY(nsIObserver)
NS_INTERFACE_MAP_END_INHERITING(nsDocLoader)

マクロになっているが、間違いなくQueryInterfaceの実装部分である。
マクロの実態はきわめて単純で、自分自身を指定されたインターフェースに対してキャストしているだけである。

nsISupportsImpl.hより

#define NS_INTERFACE_MAP_ENTRY(_interface)      NS_IMPL_QUERY_BODY(_interface)
//略...
#define NS_IMPL_QUERY_BODY(_interface)                                        \
  if ( aIID.Equals(NS_GET_IID(_interface)) )                                  \
    foundInterface = static_cast<_interface*>(this);                          \
  else

見ての通り、自分自身をキャストしているだけである。

一方で、nsDocShellにおけるgetInterfaceの実装は次のようになっている。
nsDocShell.cppより

NS_IMETHODIMP nsDocShell::GetInterface(const nsIID & aIID, void **aSink)
{
    //略...
    if (aIID.Equals(NS_GET_IID(nsIURIContentListener))) {
        //略...
    }
    else if (aIID.Equals(NS_GET_IID(nsIScriptGlobalObject)) &&
        //略...
    }
    else if ((aIID.Equals(NS_GET_IID(nsIDOMWindowInternal)) ||
              aIID.Equals(NS_GET_IID(nsPIDOMWindow)) ||
              aIID.Equals(NS_GET_IID(nsIDOMWindow))) &&
             NS_SUCCEEDED(EnsureScriptEnvironment())) {
        return mScriptGlobal->QueryInterface(aIID, aSink);
    }
    else if (aIID.Equals(NS_GET_IID(nsIDOMDocument)) &&
             NS_SUCCEEDED(EnsureContentViewer())) {
        mContentViewer->GetDOMDocument((nsIDOMDocument **) aSink);
        return *aSink ? NS_OK : NS_NOINTERFACE;
    }
    //略...
}

これを見ても分かる通り、たとえば、nsIDOMWindowをgetInterfaceで求められた場合、docShellは自分自身を返すのではなく、mScriptGlobalというメンバ変数に対し処理を丸投げしているといえる。

上記のようにQueryInterface可能なインターフェースとgetInterface可能なインターフェースはそれぞれに別々にハードコーディングされている。QueryInterface可能なインターフェースは、XPCOM Reference - XULPlanetなどを参照すればどうにか分からないでもないが、getInterface可能なインターフェースについては、少なくとも自分の調べた限りは参照できるドキュメントが存在しなかった。

ソースコードを参照するしかない、ということであれば何とも遺憾な話である。ドキュメントつくってほしいなぁ(他力本願)。