フタなしカンヅメ

徒然なるままに @happytar0

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版を使ったほうが簡単ということでしょうか。それともやり方がおかしいのか・・・?