itochin2の日記(仮)

主に備忘録。Perl、MySQL、Unity、開発管理などについて情報を残していきたい。

Module::Spyでテンプレートに渡したパラメータのテストをする

概要

JSONを返すAPIのテストなら、スキーマの確認がやりやすいのだけど
テンプレを返すコントローラってどうやってやろうか、という課題。

テンプレを表示して目視する作戦は効率悪いし、絶対漏れるし
継続的にするの無理だし、プログラムのテストとテンプレのテストが
混ざっていてイケてない感。

IFで使用するパラメータを渡しているかが知りたいのであって
分岐した結果の表示は、また別問題だと思うのです。

で、ググった。

[perl] コントローラがどのテンプレを表示したかをテストする - blog.64p.org

テンプレの名前がわかるなら、渡しているパラメータも分かるのでは!?

で、実際にやってみたらできた!

ざっくりこんな感じ。

$spy = spy_on('Text::Xslate', 'render');

# コントローラ呼ぶ
my $mech = Test::WWW::Mechanize::PSGI->new(app => $app);
$dat = $mech->get('/');
is($dat->code. 200);

my $tmpl = $spy->calls_first->[1];
my $tmpl_params = $spy->calls_first->[2];

is($tmpl, '/index.tt');
my ($ok, $error) = ThaiSchema::match_schema(
    $tmpl_params,
    type_hash({
        member_id => type_int(),
        items => type_array(
            id => type_int(),
            name => type_str(),
            num => type_int(),
        ),
        pager => type_hash({
            total_entries => type_int(),
            page => type_int(),
            hashNext => type_bool(),
            rows => type_int(),
        })
    });
);
is_deeply($tmpl_params, $expected);

calls_firstの1番目以降に、renderメソッドに渡しているパラメータが入っているので
あとは煮るなり焼くなり、好きにできて嬉しい。

まとめ

なんかページの表示がおかしいって時に、コントローラのテストを行うことで
俺のせいじゃないです問題の切り分けが素早くできるようになるはず!

ajaxでクロスドメインのAPIを叩く時にやったこと

アプリのweb viiewから、ajaxでクロスドメインAPIを実行しようとして
とても大変な思いをしたので備忘録。

概要

  • APIサーバーから、アプリのWebViewで表示するHTML(文字列)を取得する
  • HTMLの中で別サーバーのJSを読み込む。
  • JSからXMLHttpRequestAPIを実行する。
  • APIはGETとPOSTの2種類。
  • APIサーバーはPerlで、WAFにAmon2を使用。

GETが失敗する件

ログを見ると、OPTIONSリクエストに403を返していた。

What is OPTIONSリクエスト?

プリフライトリクエストという。
リクエストを送信しても安全か?サーバーがリクエストに対応しているか?
ということを調べるために、ブラウザが特定の条件を満たす場合に飛ばす。

特定の条件

以下、HTTP access control (CORS) | MDNより引用。

  • GET または POST 以外のメソッドを使用します。また application/x-www-form-urlencoded、multipart/form-data、または text/plain 以外の Content-Type とともに POST を行う場合、例えば application/xml または text/xml を用いて XMLペイロードをサーバーへ送るために POST を用いるような場合は、リクエストでプリフライトを行います。
  • カスタムヘッダをリクエストに設定します (例えば、X-PINGOTHER のようなヘッダを用いるリクエスト)。

プリフライトリクエストに対応する

以下のモジュールを使った。
Plack::Middleware::CrossOrigin - search.cpan.org

app.psgi
enable 'Plack::Middleware::CrossOrigin',
    origins => '*',
    headers => '*',
    methods => ['GET', 'POST'];

結果

PC 動いた!
iPhone 動いた!
Android 4.1.2 動いた!
Android 2.3.6 動かない!

Androidェ・・・
ググったらこんな以下のような記事がヒット
Android 2.3 の WebViwe で GET によるクロスドメインリクエストが最初の1回しか成功しない - latest log
webviewでGETはまともに動かないので、JSONPにしろとのこと。

JSONPに対応する

What is JSONP

  • JSでは、クロスドメインにアクセスできない制限がある(同一生成元ポリシー)
  • クロスドメインからJSをダウンロードすることは可能である。
  • よって、クロスドメインから、APIの戻り値が入ったスクリプトを取得することは可能である。

という小細工仕組み。
JSONPで公開しているリソースは、誰からでも参照できてしまうので
機密情報を入れると情報漏えいのリスクがあり、注意が必要。

以下のモジュールを使った。
Plack::Middleware::JSONP - search.cpan.org

app.psgi
enable 'Plack::Middleware::JSONP';

結果

PC 動いた!
iPhone 動いた!
Android 4.1.2 403 forbidden
Android 2.3.6 403 forbidden

Androidェ・・・
これは、Amon2がJSONPを拒否していた。
JSON hijacking対策として以下の条件を満たす場合は403。
・GETリクエスト
・X-Requested-Withヘッダが付いていない
Cookie送っている
UserAgentにAndroidがある ←!!

JSONPスクリプトタグからのリクエストなので、ヘッダは付けられない。

他の対策案を考える

JSONPを拒否するのをやめる

せっかくフレームワークレベルで対策しているのに、いけてない感。

POSTでGET

語感だけは楽しそうだけど、いけてない感。

GETするのをやめる

HTMLを返すときに、APIの結果を埋め込めばいいんだ!
そして、埋め込まれた結果を使って、JSでDOMを弄くろう!
(いま考えると、なぜテンプレートを使おうという発想にならなかったのか)

APIの結果をHTMLに埋め込んだ。

こんな感じに。

{"result":{"id":1, "message":"hogehoge"}}

結果

PC 動いた!
iPhone 動いた!
Android 4.1.2 動かない!
Android 2.3.6 動かない!

htmlに書いたJSに「//」が含まれていた。
そしたら、以降がコメントとみなされて動かなくなってた。

加工済みのテンプレートを返す対応

結局こうなった。

結果

PC 動いた!
iPhone 動いた!
Android 4.1.2 動いた!
Android 2.3.6 動いた!

紆余曲折あり、なんとかGETできるようになった。
うそ。GETじゃなかった。条件分岐したテンプレを返しただけ。

POSTが失敗する件

Androidだけまた動かない。
OPTIONSで200 OKを返しているが、ブラウザでabortしている。

ここは理由が分からなかったので課題。
OPTIONSを飛ばさないでPOSTする分には動作していたので、
カスタムヘッダを削る&Content-Typeに「application/x-www-form-urlencoded」を指定で対応。

結果

PC 動いた!
iPhone 動いた!
Android 4.1.2 動いた!
Android 2.3.6 動いた!

こいつ、動くぞ!
要件は満たされた。

Access-Controll-Allow-Origin:"*"ってどうなのか

全てのホストからのリクエストを許可する設定はよくないので、Originを指定したいが
Androidのweb viewはリクエストのOriginがnullになっているというやる気のなさ。

しょうがないので、特定のリクエストの時のみ、CORSヘッダを付与するようにした。

まとめ

動いたことに満足しがちだけど、クロスドメインに紐づくセキュリティリスクも
きちんと知ることが重要。
あと、Androidのweb viewでクロスドメインは罠が非常に多かったので
jsをAPIと同じホストに置くのが一番良いのではないかと思う。

参考

http://www.w3.org/TR/cors/
https://developer.mozilla.org/ja/docs/HTTP_access_control
ここを熟読して、CORSとはなんなのかをよく学ぶべき。

http://www.atmarkit.co.jp/ait/articles/0908/10/news087.html
JSONPを使用するまえに熟読して、リスクを知るべき。

クロスドメインで調べた事メモ

ajaxでクロスドメインだと動かない件で、ググったことが多かったのでメモ。

そもそもクロスドメイン制約って?

Ajaxでは同一生成元ポリシーにもとづくセキュリティ上の制約がある。
以下の1つでも違うとアクセスできない。
・ホストが違う
プロトコルが違う
・ポートが違う

じゃあどうすれば?

JSONP

JSONPの仕組み
  1. javasciprtの読み込みは別ドメインからできる。
  2. なのでjavascriptを読み込む体で、HTTPリクエストする。リクエストにはcallbackってパラメータいる
  3. そしたらjavascriptのコードが返ってくる(おや?偶然だけど中身はJSON!)

request例
/api/item/detail?callback=jsonp&id=100

response例
content-Type: text/javascript
content: jsonp({"result":{"item_id":100, "item_name":'アイテム'}})

plackアプリで実装するとき
PLACK::Middleware::JSONPを使う
パラメータに'callback'があれば、自動的にjsonを上記の形にして
sizeとかcontent-Typeを修正してくれる。
JSONJSONPの対応を、モジュールに手を入れずにできる。

JSONPの課題
  • ブラウザ依存ある

XMLHttpRequest Level2に対応したブラウザじゃないとダメ。
IE7とかはさようなら。。

  • コードの精査はできない。

危険なコードも制御できずに実行される。

  • GETはできるけどPOSTできない。

ajaxで商品の参照はできるけど、購入は無理。

Access-Control-Allow-Originヘッダを付ける

レスポンスにヘッダを付けてあげる。
上記ヘッダで指定のオリジン(プロトコル・ホスト・ポート情報)が一致しているところからの
アクセスは受け付けるヘッダ。

まとめ

今回は深遠なる理由からJSONPで対応したが、
基本的にはヘッダを付けて制御するのが王道みたい。
ヘッダの値をアプリケーションで見て、制御がいるのかフレームワークで吸収されているのかを
調べるのがTODO。

user-agentでOSとか判定するのは辛かった

user-agentを使用して、OSとかブラウザとか判定する実装した。
ようは、動作環境を満たすかチェックしたかったので。
吐きそうなくらい辛かった気持ちを忘れないためにメモを残す。

要件

まずはCPANをチェック

HTML::ParseBrowser - search.cpan.org
[OK] OSのバージョン取れる。
[OK] ブラウザ名取れる。
[NG] スマホかどうかは不明。
[NG] タブレットかは分からない。

Parse::HTTP::UserAgent - search.cpan.org
[OK] OSのバージョン取れる。
[OK] ブラウザ名取れる。
[NG] スマホかどうかは不明。
[NG] タブレットかは分からない。

HTTP::UserAgentStringParser - search.cpan.org
そもそも外部と通信する必要あるのが懸念材料。

Woothee - search.cpan.org
[NG] OSのバージョン取れない。
[OK] ブラウザ名取れる。
[OK] スマホかどうか分かる。
[NG] タブレットかは分からない。

検討したこと

CPANモジュールはどれも少しずつ惜しい。

そしてアプリケーションで利用するためには、
windowsは7も8もwindowsとして管理したり、'mac'という文字列じゃなくて定数にしたかったり
様々な加工が必要だったので、泥臭く実装することにした。

面倒だったポイント

  • IEMSIEって入っていたが、IE11から急になくなった
  • WindowsXPは32bitと64bitで違う文字列(NT5.1とNT5.2)
  • OS Xのバージョン区切りが「_」と「.」のパターンある
  • OS X のバージョン終わりが「)」と「;」のパターンある
  • androidはMobileがないヤツがタブレットだが、例外もある
  • ChromeChrome.*Safariってなってる
  • テストケース用意するのがしんどい。

実装されたもの(いろいろ出していけない部分は削っている

感想

もうUserAgentで判定する世界はなくなればいいのに。
好き勝手に文字列作りすぎ。

perlの文字列をバイト数で切り取るヤツ

APIに渡す文字列は25文字(50byte)でよろしく、
という要件に対応するサブルーチンを実装した時のメモ。

サブルーチンでは以下の3つを考慮する。
・文字数制限を満たす
・バイト数制限を満たす
・文字列として成立する(単純にバイト数でぶった切ると、文字列がおかしくなる


実装の時に調べてしったこと

・bytesプラグラマは非推奨
http://perldoc.perl.org/bytes.html

雑感

文字列は入り口でデコードして、出口でエンコードなんだから、
encodeしてlengthを取るのはごく普通なことだと思った。

あと、文字数がバイト数より大きいくなることってありえるのかなぁ。
よくて同じな気がする。
そうだったら、文字列の長さチェックは省略してもよいのかもしれぬ。

所有ユーザーとグループを同時に変更する(しかも再帰的に

git pullしたらpermissionがどうとか言われて怒られた。

以前rootでgit pullやってしまった記憶がなきにしもあらずなので
指摘されているファイルの権限を修正した。

表示されたエラー

remote: Counting objects: 120, done.
remote: Compressing objects: 100% (95/95), done.
remote: Total 98 (delta 75), reused 0 (delta 0)
error: insufficient permission for adding an object to repository database .git/objects
fatal: failed to write object
fatal: unpack-objects failed

対処のために打ったコマンド

chown -R user:user /home/project/.git/objects

たまにしか使わないから毎回ググってるけど、そろそろ覚えよう。

rubyの環境構築したメモ

環境はCentOS6.5

rbenvを使ったrubyのインストール

ここを見てやった
http://qiita.com/inouet/items/478f4228dbbcd442bfe8

bundlerのインストール

bundlerはrubyのパッケージのバージョン管理。
bundl initは、Gemfileを置くディレクトリで実行。今回はホームディレクトリ。

gem install bundler
bundl init

用語整理

本体の管理 パッケージ管理 パッケージのバージョン管理 管理ファイル
ruby rbenv gem bundle Gemfile
perl plenv cpanm cartion cpanfile