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

シリコンバレーで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からの卒業。

Twitterを始めてみた

今更にも程があるのですが、Twitter始めてみました。

http://twitter.com/thorikawa

IPA未踏の情報がTwitterで発信され始めて、あーこれからはTwitterで情報発信していく時代なんだなーと実感して始めた次第。140字の中で表現する醍醐味をこれから味わっていく予定。

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

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

    • -

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

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

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

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

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

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

mixiアプリカンファレンス 2009に行ってきました

定員に対して10倍の応募があったというmixiアプリカンファレンス 2009に当選したので行って参りました!

mixi笠原社長・Googleの辻野社長の講演を始めとした錚々たる顔ぶれで、プレスも多く詰めかけており、mixiアプリにかける周囲の期待の大きさを実感できただけでも大きな収穫でした。

僕個人が今現在mixiアプリに抱いている印象は、扱えるデータがまだまだ少ないので、アイデアだけで勝負!みたいなものが多いかなあと。もっと色々なデータ(レビューのデータとか)が使えるようになると、アイデアだけでなく特殊な技術を応用したアプリとかが出てきて、面白くなっていくのではと思ってます。あと、今回はほとんどクローズアップされなかったけどmixiコネクトには相当期待しています。
それと、mixiアプリのモバイル版があるとの発表がありました。これはまだ可能性が未知数。仕様が明日4/24に発表されるとのことでこちらも楽しみにしています。

懇親会では、毎度お世話になってるゴーガの小山さんに紹介して頂いて笠原社長や小山技術顧問とも名刺交換させて頂きました。人とのつながりは大事ですね、紹介してもらえるというのはありがたいことです。感謝!

はてなブックマーク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の開発に興味を持っている方、質問などお気軽にお願いします。
(その前にコードをリファクタリングした方がいいとは思いつつも・・・)