Creating Sandboxed HTTP Connections

by 1 contributor:

導入

Gecko 1.8.1 (Firefox 2) から、ユーザの Cookie に影響しないサンドボックス内の HTTP 接続を作成できるようになりました。この記事では JavaScript の XPCOM から HTTP 接続を行うための基礎を扱いますが、C++ の XPCOM にも簡単に移植できるはずです。

HTTP 接続を確立する

URL (文字列に格納されている) から HTTP 接続を確立するための最初の手順として、その URL から nsIURI を作成します。nsIURI は XPCOM における URI の表現で、URI をクエリしたり操作するのに便利なメソッドを持っています。文字列から nsIURI を作成するには、nsIIOServicenewURI メソッドを使います。

// IO サービス
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
                          .getService(Components.interfaces.nsIIOService);

// nsIURI を作成する
var uri = ioService.newURI(myURLString, null, null);

nsIURI が作成されれば、それから nsIIOServicenewChannelFromURI メソッドを使って nsIChannel を生成できます。

// その nsIURI に対するチャンネルを取得する
var channel = ioService.newChannelFromURI(uri);

接続を開始するには asyncOpen メソッドを呼び出します。このメソッドはリスナとそのリスナのメソッドに渡されるコンテキストの 2 つの引数を取ります。

channel.asyncOpen(listener, null);

HTTP の通知

上で述べたリスナは nsIStreamListener で、HTTP リダイレクトやデータの取得といったイベントについての通知を受けます。

  • onStartRequest - 新しいリクエストが開始される時に呼ばれる。
  • onDataAvailable - 新しいデータが取得できるようになった。これはストリームなので、(返されるデータのサイズやネットワークの状態などによっては) 複数回呼ばれることがある。
  • onStopRequest - リクエストが完了した。
  • onChannelRedirect - リダイレクトが発生すると、新しく nsIChannel が作成され、古い方と新しい方が引数として渡される。

nsIStreamListener は Cookie をサポートしておらず、Cookie の通知に対しては他のリスナを使う (次の節で取り上げます) ため、現在使用されているチャンネルはグローバル変数として格納する必要があります。必要なメソッドを全て実装した JavaScript ラッパを使い、指定したコールバック関数を接続が完了した時に呼び出すのが、普通は最もよい方法です。

// グローバルチャンネル
var gChannel;

// チャンネルを初期化する

// IO サービス
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
                          .getService(Components.interfaces.nsIIOService);

// nsIURI を作成する
var uri = ioService.newURI(myURLString, null, null);

// その nsIURI に対するチャンネルを取得する
gChannel = ioService.newChannelFromURI(uri);

// リスナを取得する
var listener = new StreamListener(callbackFunc);

gChannel.notificationCallbacks = listener;
gChannel.asyncOpen(listener, null);

function StreamListener(aCallbackFunc) {
  this.mCallbackFunc = aCallbackFunc;
}

StreamListener.prototype = {
  mData: "",

  // nsIStreamListener
  onStartRequest: function (aRequest, aContext) {
    this.mData = "";
  },

  onDataAvailable: function (aRequest, aContext, aStream, aSourceOffset, aLength) {
    var scriptableInputStream = 
      Components.classes["@mozilla.org/scriptableinputstream;1"]
        .createInstance(Components.interfaces.nsIScriptableInputStream);
    scriptableInputStream.init(aStream);

    this.mData += scriptableInputStream.read(aLength);
  },

  onStopRequest: function (aRequest, aContext, aStatus) {
    if (Components.isSuccessCode(aStatus)) {
      // リクエストは成功した
      this.mCallbackFunc(this.mData);
    } else {
      // リクエストは失敗した
      this.mCallbackFunc(null);
    }

    gChannel = null;
  },

  // nsIChannelEventSink
  onChannelRedirect: function (aOldChannel, aNewChannel, aFlags) {
    // リダイレクトしたら、新しいチャンネルを格納する
    gChannel = aNewChannel;
  },

  // nsIInterfaceRequestor
  getInterface: function (aIID) {
    try {
      return this.QueryInterface(aIID);
    } catch (e) {
      throw Components.results.NS_NOINTERFACE;
    }
  },

  // nsIProgressEventSink (実装しないとうっとうしい例外を引き起こす)
  onProgress : function (aRequest, aContext, aProgress, aProgressMax) { },
  onStatus : function (aRequest, aContext, aStatus, aStatusArg) { },

  // nsIHttpEventSink (実装しないとうっとうしい例外を引き起こす)
  onRedirect : function (aOldChannel, aNewChannel) { },

  // XPCOM インターフェイスに見せかけているので、QI を実装する必要がある
  QueryInterface : function(aIID) {
    if (aIID.equals(Components.interfaces.nsISupports) ||
        aIID.equals(Components.interfaces.nsIInterfaceRequestor) ||
        aIID.equals(Components.interfaces.nsIChannelEventSink) || 
        aIID.equals(Components.interfaces.nsIProgressEventSink) ||
        aIID.equals(Components.interfaces.nsIHttpEventSink) ||
        aIID.equals(Components.interfaces.nsIStreamListener))
      return this;

    throw Components.results.NS_NOINTERFACE;
  }
};

ちょっとしたメモ: グローバルスコープにチャンネルを格納するのは (特に拡張機能では) あまり良い方法ではありませんが、コードを読みやすくするためにそうしました。全ての実装をクラスの中に入れ、チャンネルをメンバとして格納した方が良いでしょう。

 function myClass() {
   this.mChannel = null;
   ...
   var listener = new this.StreamListener(callbackFunc);
   ...
 }
 
 myClass.prototype.StreamListener = function (aCallbackFunc) {
   return ({
     mData: "",
     ...
   })
 }

リクエストを送る時、その URL に対応する Cookie が HTTP リクエストと共に送られます。また HTTP レスポンスにも Cookie が含まれることがあり、ブラウザはそれを処理します。Mozilla 1.8.1 (Firefox 2) 現在では、これら 2 つのケースを横取りする事が出来ます。

これにより、例えばユーザが Web メールのアカウントにログインしていても、同じドメインの違うアカウントをユーザの Cookie に変更を加えることなくチェックすることが出来ます。

オブザーバサービス (nsIObserverService) は通知全般を送るのに使われ、その中には Cookie に関するものが 2 つ含まれています。特定のトピックに対するオブザーバを追加するには addObserver メソッドを使います。これは 3 つの引数を取ります。

  • nsIObserver を実装するオブジェクト
  • 捕捉 (listen) するトピック。Cookie に関する 2 つのトピックは、
    • http-on-modify-request - Cookie データがリクエストに読み込まれた後、リクエストが送られる前に起こる。
    • http-on-examine-response - レスポンスが受け取られた後、Cookie が処理される前に起こる。
  • 引数として渡されたオブザーバに対して弱い参照 (weak reference) を保持するかどうか。false を使ってください。

メモリリークを回避するため、どこかの時点でオブザーバを削除しなければなりません。removeObserver メソッドはリスナオブジェクトとトピックを引数に取り、それを通知リストから削除します。

上記のストリームリスナと同じように、nsIObserver を実装したオブジェクトが必要になります。これが実装しなければならないのは、observe というメソッド一つだけです。observe メソッドには 3 つの引数が渡されます。2 つの Cookie トピックに関して言えばこの引数は、

  • aSubject: この通知を引き起こしたチャンネル (nsIChannel)。
  • aTopic: 通知トピック。
  • aData: この 2 つのトピックに関しては null

オブザーバは登録されたトピックの通知をあらゆる接続から受け取るので、リスナ側でその通知が自分のコードが作成した HTTP 接続からのものかを確認しなければなりません。通知を引き起こしたチャンネルは 1 つめの引数として渡されるので、それを前の節でグローバルスコープに格納されたチャンネル (gChannel、リダイレクトが起こるたびに更新される) と比較します。

// nsIObserver を実装するオブジェクトを作成する
var listener = {
  observe : function(aSubject, aTopic, aData) {
    // まず自分で作った接続かどうか確かめる
    if (aSubject == gChannel) {
      var httpChannel = aSubject.QueryInterface(Components.interfaces.nsIHttpChannel);
      if (aTopic == "http-on-modify-request") {
         // ...
      } else if (aTopic == "http-on-examine-response") {
         // ...
      }
    }
  },

  QueryInterface : function(aIID) {
    if (aIID.equals(Components.interfaces.nsISupports) ||
        aIID.equals(Components.interfaces.nsIObserver))
      return this;
    throw Components.results.NS_NOINTERFACE;
  }
};

// オブザーバサービスを取得して 2 つの Cookie トピックに対して登録する
var observerService = Components.classes["@mozilla.org/observer-service;1"]
                                .getService(Components.interfaces.nsIObserverService);
observerService.addObserver(listener, "http-on-modify-request", false);
observerService.addObserver(listener, "http-on-examine-response", false);

最後に Cookie を操作します。Cookie を操作するには、QueryInterface (QI) を使って nsIChannelnsIHttpChannel に変換する必要があります。

var httpChannel = aSubject.QueryInterface(Components.interfaces.nsIHttpChannel);

Cookie は実際には HTTP ヘッダの一部であり、nsIHttpChannel はヘッダを扱う 4 つのメソッドを備えています。2 つはリクエストヘッダを取得および設定するもので、もう 2 つはレスポンスヘッダを取得および設定するものです。リクエストに対しての Cookie ヘッダは "Cookie" という名前で、レスポンスに対しては "Set-Cookie" です。

  • getRequestHeader(aHeader) - 指定されたヘッダに対するリクエストヘッダの値を返す。
  • setRequestHeader(aHeader, aValue, aMerge) - リクエストヘッダの値を設定する。aMergetrue なら新しい値が追加され、そうでなければ古い値が上書きされる。
  • getResponseHeader(aHeader) - 指定されたヘッダに対するレスポンスヘッダの値を返す。
  • setResponseHeader(aHeader, aValue, aMerge) - レスポンスヘッダの値を設定する。aMergetrue なら新しい値が追加され、そうでなければ古い値が上書きされる。

これらのメソッドは Cookie が処理されたり送られる前に変更するのに必要な機能を全て備えており、これによりユーザの Cookie に影響しないサンドボックス内の Cookie 接続が可能になります。

HTTP リファラ

HTTP リクエストにリファラを設定する必要があるなら、nsIChannel を作成した後、それが開かれるまえに 2 つの手順を追加しなければなりません。まず、リファラ URL に対して nsIURI を生成します。前と同じように、nsIIOService を使います。

var referrerURI = ioService.newURI(referrerURL, null, null);

次に、nsIChannelnsIHttpChannel に QI し、referrer プロパティを先ほど生成した nsIURI に設定します。

var httpChannel = channel.QueryInterface(Components.interfaces.nsIHttpChannel);
httpChannel.referrer = referrerURI;

HTTP POST を作成する

HTTP POST を作成するには、nsIChannel を作成した後にいくつかの手順を追加する必要があります。

まず、nsIInputStream のインスタンスを作成し、その後 setData メソッドを呼び出します。1 つめの引数は文字列としての POST データで、2 つめの引数はそのデータの長さです。この場合ではデータは URL エンコードされるので、文字列は foo=bar&baz=eek のようになっていなければなりません。

var inputStream = Components.classes["@mozilla.org/io/string-input-stream;1"]
                  .createInstance(Components.interfaces.nsIStringInputStream);
inputStream.setData(postData, postData.length);

次に、nsIChannelnsIUploadChannel に QI します。それの setUploadStream メソッドを、nsIInputStream とその形式 (この場合は "application/x-www-form-urlencoded") を渡して呼び出します。

var uploadChannel = gChannel.QueryInterface(Components.interfaces.nsIUploadChannel);
uploadChannel.setUploadStream(inputStream, "application/x-www-form-urlencoded", -1);

バグにより、setUploadStream を呼び出すと nsIHttpChannel が PUT リクエストにリセットされるので、リクエストタイプを POST に設定します。

// 順番が重要 - setUploadStream は PUT にリセットする
httpChannel.requestMethod = "POST";

ドキュメントのタグと貢献者

Contributors to this page: Shoot
最終更新者: Shoot,