PHP版Shindigが上手く動かない

オープンソーシャルのアプリを開発する上で、必須といえるShindigShindigでコンテナを構築すればローカルでオープンソーシャルの開発をすることが出来ます。
そこでちょっとハマったことを記録として残しておきます。

まずShindigには、色々なバージョンがある上にJava版とPHP版の二種類が存在します。
以前開発に使っていたのはJava版で、PHPで上手く動かすことが出来なかったため使っていました。が、、、HAS_APPフィルタがなぜか上手く動かない事に気づき、調べてみたもののJavaがほとんど分からず、これを機会にPHP版を新たにインストールしてみた訳です。

Shindigをインストールする

ShindigApacheの所で開発されているようで、下記からダウンロードできます。
Shindig – Download
今回は、Currentの「shindig-1.1-BETA5-incubating-php.zip」を使いました。SVNリポジトリから落とすと、Java版も含まれているため、ディレクトリパスの設定がちょっとめんどうだと思います。

zipを展開し、Webからアクセス出来るように各々で設定してください。注意点は、.htaccessも含まれており、mod_rewriteも必要になるということです。

<VirtualHost *:80>
ServerName shindig.localhost.man
DocumentRoot /home/wwww/shindig # zipを展開したディレクトリ
<Directory />
AllowOverride All # .htaccessを読み込めるようにする
</Directory>
</VirtualHost>

URLパラメータ付きでも動くようにする

上記のようにインストールすれば、とりあえずは動くのですが、僕が使う上でちょっと不便な部分があったので修正しました。
それは、コンテナのURLにパラメータが指定されている場合に上手く動かなかった事です。
正しくパラメータを処理できず、「400 – Bad Request」となってしまいます。

http://shindig.localhost.man/gadgets/files/container/container.html?view=canvas
こんな感じで引数で画面を切り替えていました。

# src/gadgets/servlet/FileServlet.php
# 40行目の下にパラメータを無視するようなコード追加
#
$file = str_replace(Config::get('web_prefix') . '/gadgets/files/', '', $_SERVER["REQUEST_URI"]);
$file = Config::get('javascript_path') . $file;
// ここを追加
$file = substr($file, 0, strlen($file) - strlen(strstr($file, '?')));
// make sure that the real path name is actually in the javascript_path, so people can't abuse this to read
// your private data from disk .. otherwise this would be a huge privacy and security issue
if (substr(realpath($file), 0, strlen(realpath(Config::get('javascript_path')))) != realpath(Config::get('javascript_path'))) {

署名付きリクエストに対応する

署名付きリクエストとは何か、それはオープンソーシャル開発をする上、外部サーバを呼び出したい場合にとても重要になってきます。「makeRequest」というJavaScript関数を使用し、外部サーバに非同期にリクエストを送るわけですが、ここで重要なのは、リクエストが改ざんされないように署名するということです。

Shindigで署名付きリクエストを使うために、opensslで秘密鍵と証明書を作成します。

$ openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout test.pem -out test.pem -subj '/CN=mytestke
$ openssl pkcs8 -in test.pem -out private.key -topk8 -nocrypt -outform PEM

とりあえず: [Apache Shindig][お勉強][OpenSocial] メモ118 gadgets.io.makeRequest 認証認可タイプSIGNEDをやってみる(RSA-SHA1で署名)

出来た証明書と鍵をcertsディレクトリに保存し、Shindigの設定ファイルを書き換えます。

# config/container.php
'private_key_file' => realpath(dirname(__FILE__) . '/../certs') . '/private.key',
'public_key_file' => realpath(dirname(__FILE__) . '/../certs') . '/test.pem',
'private_key_phrase' => '',

これで問題なしと思われたのですが、上手く署名付きリクエストをおこなう事が出来ませんでした。

パラメータ付きURLから上手くmakeRequestがおこなえない

makeRequestで外部サーバを呼び出すときに、パラメータを付加すると上手く動かないという現象が発生しました。

var url = 'http://localhost/gadget/canvas?ts=1277893054';
gadgets.io.makeRequest(url, null, this.makeParams(data));

タイムスタンプ値を末尾に付加してリクエストを送っていたのですが、「?ts=1277893054」が問題で、このパラメータを上手く処理出来ていないために失敗しているようでした。

# src/gadgets/oauth/OAuth.php
# 186行目のbuild_signatureが実際に署名をおこなっている箇所になります。
public function build_signature(&$request, OAuthConsumer $consumer, $token) {
$base_string = $request->get_signature_base_string();
// Fetch the private key cert based on the request
$cert = $consumer->getProperty(OAuthSignatureMethod_RSA_SHA1::$PRIVATE_KEY);
// Pull the private key ID from the certificate
//FIXME this function seems to be called both for a oauth.json action where
// there is no phrase required, but for signed requests too, which do require it
// this is a dirty hack to make it work .. kinda
if (! $privatekeyid = @openssl_pkey_get_private($cert)) {
if (! $privatekeyid = @openssl_pkey_get_private($cert, Config::get('private_key_phrase') != '' ? (Config::get('private_key_phrase')) : null)) {
throw new Exception("Could not load private key");
}
}
// Sign using the key
$signature = '';
if (($ok = openssl_sign($base_string, $signature, $privatekeyid)) === false) {
throw new OAuthException("Could not create signature");
}
// Release the key resource
@openssl_free_key($privatekeyid);
return base64_encode($signature);
}

openssl_pkey_get_private関数によって、秘密鍵を取得し、$base_stringに対して署名していることが分かります。この$base_stringの中身はこうなっていました。

POST&http%3A%2F%2Flocalhost%3A3000%2Fgadget%2Fcanvas&dummy%3D1%26oauth_consumer_key%3Dcont%26oauth_nonce%3D96bd6e6d4103d3c5197f26d5dd6e3463%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1277893517%26oauth_token%3D%26opensocial_app_id%3Dappid%26opensocial_app_url%3Durl%26opensocial_owner_id%3Djohn.doe%26opensocial_viewer_id%3Djohn.doe%26ts%3D1277893516%26xoauth_public_key%3Dhttp%253A%252F%252Fshindig.localhost.man%252Fpublic.cer%26xoauth_signature_publickey%3Dhttp%253A%252F%252Fshindig.localhost.man%252Fpublic.cer

この時に「ts%3D1277893516」の文字列があり、パラメータ「ts」も含めて署名されている事が分かります。

続いて実際に外部サーバにリクエストを送る部分です。

# src/gadgets/SigningFetcher.php
# 111行目のsignRequestが送信リクエストを作成します。
# 174行目にクエリーストリングを処理する箇所があり、送信先リクエストにパラメータを追加して渡しています
$newQuery = '';
foreach ($req_req->get_parameters() as $key => $param) {
if (! isset($forPost[$key])) {
$newQuery .= urlencode($key) . '=' . urlencode($param) . '&';
}
}
# 問題の箇所
// and stick on the original query params too
if (isset($parsedUri['query']) && ! empty($parsedUri['query'])) {
$oldQuery = array();
parse_str($parsedUri['query'], $oldQuery);
foreach ($oldQuery as $key => $val) {
$newQuery .= urlencode($key) . '=' . urlencode($val) . '&';
}
}

一見問題ないように見えますが、$req_req->get_parameters関数が返す値には、既にクエリーストリングも処理されており、二重でクエリーストリングが処理されていることになります。
パラメータの二重化によって、署名されたリクエストと異なる値になってしまい、外部サーバでの署名確認時にエラーとなってしまいます。

パラメータが二重処理されたリクエストです。パラメータ「ts」が二つあることが分かります。

http://localhost/gadget/canvas?oauth_nonce=0c8c8040978beed7b0221f655ef50303&oauth_timestamp=1277893863&oauth_consumer_key=cont&ts=1277893863&opensocial_owner_id=john.doe&opensocial_viewer_id=john.doe&opensocial_app_id=appid&opensocial_app_url=url&oauth_token=&xoauth_signature_publickey=http%3A%2F%2Fshindig.localhost.man%2Fpublic.cer&xoauth_public_key=http%3A%2F%2Fshindig.localhost.man%2Fpublic.cer&oauth_signature_method=RSA-SHA1&oauth_signature=KTo5X7QxKg8mSpDiaGjljeyKAlmdMLO%2BbPrAr0qYycqAzqTSw8t44p2lXmdRZendFwEpbKLPZy9tAoJ98X9YVkOHjXsp6D19%2BtFBCgSQDJzuvWxHC6xKiRnw2iR0M0HcKSNeMf6x45W%2FJM9rwxT%2BdUYrDZ%2BO4IKbq1etvQj6nWs%3D&ts=1277893863&

src/gadgets/SigningFetcher.phpの181行目〜187行目までをコメントアウトすることで回避できます。

html_sanitize is not defined

これでようやく署名付きリクエストに対応出来たかと思われましたが、次に新たな問題が発生しました。
コンテナ内で「Tabs」Featureを利用した時に「html_sanitize is not defined」というJavaScriptエラーが出ることです。
どうやら、core Featureでhtml-sanitizer.jsというのを読み込むコードが足りないようです。

# src/gadgets/GadgetFeatureRegistry.php
// Make html-santitization work see SHINDIG-346
if ($content == 'res://com/google/caja/plugin/html-sanitizer.js') {
$content= 'http://google-caja.googlecode.com/svn/trunk/src/com/google/caja/plugin/html-sanitizer.js';
} 

このような記述があり、html-sanitizer.jsだけ特別な処置がされているようです。
今回は、Tabs Featureにhtml-sanitizer.jsを読み込ませるコードを追加します。具体的には、feature.xmlに以下のように追加します。

# features/src/main/javascript/features/tabs/feature.xml
<feature>
<name>tabs</name>
<gadget>
<!-- for gadgets.util.sanitizeHtml -->
<script src="res://com/google/caja/plugin/html-sanitizer.js"/>
<script src="tabs.js"/>
<script src="taming.js"/>
</gadget>
</feature>

これでまともに使えるようになりました。PHP版Shindigは地雷が多いのでJava版を使ったほうが簡単ということでしょうか。それともやり方がおかしいのか・・・?

ブラウザでP2P通信、ファイル転送が出来るのか

P2Pと聞くとマイナスのイメージを持ってしまいますが、P2P技術が使われているプロダクトは意外に多いのではないかと思います。

もちろん、P2Pを利用する上での長所・短所はあります。
長所は、通信帯域を多く使うアプリケーションの場合、サーバを介さずに通信が出来るためコストを抑えることができ、Winnyに代表されるように匿名性やスケーラビリティといった耐障害性の観点からもメリットがあります。
逆に短所は、実装が困難な点やノード管理の複雑さ、接続先のネットワーク環境に左右されうる点などがあると思います。

特に通信帯域を多く使うような場面での効果は絶大です。動画や音声配信などの通信帯域を使う場面はどんどん増えていっています。
この状況下(1年くらい前?)で、FlashPlayer1.0から実装されたのがRTMFP(Real Time Media Flow Protocol)というプロトコルです。
これはUDPを使い、ユーザ間同士で直接通信することができるような夢のような機能です。

少し気になるのはUDP?というわけですが、これにはいくつか理由があるようです。動画や音声送信などのリアルタイム性(遅延が少ない)を求める場面で有利ということや、NAT越えもし易いってこともあるんじゃないでしょうか。

コネクションレスUDPでは、遅延が少ないといってもちゃんと届いている不安な感じですが、信頼性を上げるために輻輳処理や暗号化などが取り入れられているそうです。詳しくは下記参照。
Flash Player 10.1 と RTMFP – akihiro kamijo

あともう一つ、このプロトコルではStratusという中央サーバが必要であり、ハイブリッドP2Pであるということが言えます。Stratusは現在Adobeのサイトにて無償開放されていますが、将来的にはFMSに取り込まれるのではないかと思います。
Cirrus | Real Time Media Flow Protocol (RTMFP) – Adobe Labs

NAT越えは、UDPホールパンチングという方法を使っているそうで、最初に中央サーバに接続する事で実現しているようです。アウトバウンドのUDPが使える環境で、なおかつルータがNATトラバーサルに対応している必要があるという条件は限られますが、多くの環境では使えるのではないかと思います。信頼性を重視するサービスの場合、既存のRTMP等のプロトコルと併用します。

そして6月になり、FlashPlayer10.1が正式リリースされました。新たに10.1で導入された機能にピアアシストネットワークというものがあります。
これはノード同士をグループ化することができ、自律したネットワークを構成してくれるという優れものです。詳しくは下記参照。
Flash Player 10.1 と RTMFP (ピアアシストネットワーク) – akihiro kamijo

そんなこんなでFlashでP2Pを利用できる環境がばっちり整ってきた訳で、早速何か作ろうかと考えて出来たのが、ブラウザ間でファイルをP2P転送することが出来るWebサービスです。
We're sorry, but something went wrong (500)

まだ公開したばかりですが、上に書いたようにビアアシストネットワークを使ったりすれば、もう少し便利になるのではないかという感じです。他のサービスにも応用出来そうなので色々作れればいいなーと思います。