ゾウさんが好きです。でもjsonengineのほうがもーっと好きです

さる案件で appengine を使った restful なサーバが必要になりました。django をメインの環境に採用して以降、python を使い続けてきたことも手伝って何の疑問も無く webapp を使い始めたのですが、いかんせんテスト環境が悪杉です。


まず、公式に記述が一切無いこと。百歩譲って有志の方が公開してくださっている情報で良しとしても、ローカルとプロダクションでの挙動が違う時もあって、ちょっと面喰らいました。仮に datastore をローカルで完全再現できない(プログラム的に。あるいはライセンス的に敢えて)のであれば、双方で同一テストを実行できる環境くらいは、あらかじめ用意しておいてくれても罰は当たらんと思うのです。

AppEngine SDK標準テスト用環境の問題 - tagomorisのメモ置き場


そんな折り、webapp での良質なテスト環境を求めてネットを彷徨っていたところ ktrwjr のことを知りました。

ところで出来上がったあと公開用の準備をやってたら、GAE/J側ではslim3がkotoriを組み込んだという話が。おおお。 テストケースのベースクラス GAETestBase を作った - tagomorisのメモ置き場

python のテスト環境の紹介で java用が既知となる、というのも何やらおかしなものですが、お試してみると、正に望んでいたそのものでありませんか!

java ならば、公式に記述もあります。個人的には python への特段のこだわりも無いので、「昔取った杵柄」よろしく、さっさと java への移行を決めました。


さて、問題はどの framework を採用するか、です。datastore については slim3 で文句無し、といったところでしょうか。ただ、restful については少し支援機能が欲しいところではあります。java では目移りするくらい沢山あるのですが、GAE で使いものになる、という括りでは相当絞られてくるのでしょう。

以上のサイトを参考に下記をピックアップしてお試ししてみました。

  • Jersey
    • そのままでは GAE で稼働しないが、回避方法有り。
    • ドキュメントにはテスト環境への言及もあり好感。
    • 全体的にシンプルで理解しやすい構成に好印象。
    • slim3 の SpinUpTime を見てしまうと遅い部類。(難点)
  • Restlet
    • GAE と GWT に公式対応。
    • リソースを意識したコード構成に好印象。
    • SpinUpTime も実装状況を考えれば頑張っている方だと思う。
    • テスト環境がまったく用意されていない。google先生でも確定的な情報を取得できない。(難点)
  • jsonengine
    • framework と言うより GAE アプリケーションという区分。
    • slim3 で記述されているので、上記二つで問題となったテストと SpinUpTime は文句の余地が無い。
    • 保存形式は JSON のみ。希望の挙動(対XSSなど)に変更したい場合はソースを読んで修正する必要有り。(難点)

個人的には Jersey か Restlet が一押しレベルなのですが、やはりテスト環境の有無や SpinUpTime の遅さは最終的にサービスを利用するユーザ視点ではマイナスでしょう。ある程度、必要とされる挙動へのコード修正にも目処が付いたので、今回は jsonengine を採用することにしました。


P.S.
要望として issues にも投稿してみたいとも思うのですが、英語というのがいささかハードルが高いです。っていうか、そんなこと言ってちゃイケませんね。英語をもっと勉強せねば!

結局残ったのは水ロケットとはやぶさだった

先日シャープの亀山モデル消滅の一報が届きました。

シャープが誇る液晶テレビのブランド「亀山モデル」が消滅へ

亀山工場と言えば、地方自治体の垂涎の的。工場誘致(地方税収確保)の成功事例の代表格とも言われた存在でしたが、わずか数年で幕を閉じることになってしまいました。いかな最先端の技術を以ってしても、国際的な水平分業の前にはわずか10年足らずでコモディティ化の波の中に飲み込まれてしまう、という事実をまざまざと見せ付けられた。そんな事例に亀山工場は変貌してしまいました。

技術立国日本は一体どこへ向かえば良いのでしょうか。


そんな折り、60億キロという途方も無い距離を走破した「はやぶさ」の地球帰還がいよいよ迫ってきていました。その存在を知った当初、ニコ動で有志の方が製作された映像を観た時は、ただただ目から涙が溢れ出ました。

イオニアボイジャーをニュース体験した世代(年齢がバレてしまいますね笑)としては、木星土星の映像にワクワクしながらも、「月軌道を超える」ことは永遠の別れであって、現実は宇宙戦艦ヤマトのようにはいかない、ということも実感しています。

ゆえに、深淵なる宇宙から帰還する。ましてや機体は満身創痍ともなれば、涙腺は緩み放しになってしまうのでしょう。正に「オカエリナサλ」であります(笑)

(ちなみに、そのボイジャーも 2号が打ち上げから 30年以上の月日を経ていよいよ太陽系を離脱しようとしています。太陽圏を脱出する初の探査機:ボイジャー2号に不具合 | WIRED VISION)


私は、そんな「はやぶさ」のことを知れば知るほど、宇宙開発(探査)こそが日本の進むべきもう一つの道ではないか、子どもたちに未来への希望を示す道標になるのではないか、と思うようになりました(ちなみに、もう一つは深海探査)。

はやぶさ」に代表される宇宙への取り組みがいかに大きな日本の誇りであり、国民にとって、特に「子どもたち」にとって未来に希望を抱く大きな存在 「はやぶさ」は日本の科学技術の誇り:日経ビジネスオンライン

宇宙は決して理系だけの分野ではありません(極端な例であれば、宇宙飛行士さんのメンタル面に心理学が必要になりますよね)。正に我々の総合力が試されるフィールドです。ゆえに特定の才能に左右されず、意欲さえあれば挑戦できる未開発の領域が広がっていると思いますし、そのような領域こそが他の追随を許さない先行者利益を確保できる「技術立国日本」に相応しい立ち位置だと思うのです。


そこで、「興味は幼いころから」ではありませんが、子どもたちが宇宙に関心を持ってくれるよう、星空少年だったころの知識を総動員して最近はよく宇宙の話をするようにしています。子どもたちもそれなりに惹かれるものがあるようで、

宇宙はどうやって生まれたの? / 神さまがこうやって...手をパンと打って生まれたんだよ
宇宙に果てはあるの? / ホーキング先生が言うには、あるそうだよ
宇宙の果ての向こうはどうなってるの? / ホーキング先生曰く、そのまた向こうにも別の宇宙があるそうだよ
銀河ってキレい。 / そうだね

とまぁ、質問攻めにあっています。流行りものではありませんが、全天周映像 HAYABUSA -BACK TO THE EARTH- も観に行きました。ふと、子どもたちの方を見ると涙をボロボロ流していて、それなりに心に響くものがあったようです。


そんな中、こんなニュースが舞い込んできました(ようやく本題に到達しました苦笑)。

水ロケット めざせ世界大会  - 山梨日日新聞 みるじゃん

こちらは、まだまだ幼ない子どもたち相手に宇宙への関心を高めている真っ最中ですが、中学生のお兄ちゃんたちは、

田辺君はもともと科学には興味があったといい、「ロケットを飛ばすことが小さいころからの夢。今度は自分自身で作ったロケットを飛ばすチャンスなので頑張りたい」と話している。

もうマジで夢に向かっている。ガチでどんどん挑戦してくれちゃってる。うんうん。いいぞ。君たちのその想いが未来への原動力になるんだから。このニュースを読んだ時オジサン、本当に嬉しくなってしまいました。

ところが、先日ある飲み会に参加して、先輩のご子息たちと知り更にビックリ。幼いころに会ったことがある、あの子たちがこんなスゴい挑戦をしている。先のボイジャーではありませんが、何やら時間を越えたような感覚を覚えましたし、こういう子たちがいる限り、まだまだ日本も捨てたもんじゃないとも思いました。

いやぁ俄然オジサン、嬉しくなってしまいましたよ。こうなったら、何がなんでも世界大会へ。応援していますよ!

追伸: 8/7(土)に JAXA で開催されたました国内選抜を無事突破しました!次はオーストラリアでの世界大会です。頑張れ!

現代はjerseyを見失った時代だ

jsonengine のおかげで java の Restful な framework に興味津々。そこで色々と漁ってみました。すると...


JSONICとSlim3 DatastoreによるREST APIサービス - GeekFactory

JSONベースのRESTサーバにJerseyを使っているのですが、App Engineの実環境ではspin-up timeが長いので困っていました。mixiアプリの表示に10秒もかかると見る気が失せてしまいます。

とのことなので、実際どのくらいか試してみました。

こちらの (RESTful Webservices with Java and Jersey (JAX-RS) - Tutorial) Hello クラスをそのまま本番環境にデプロイして SpinUp 速度を簡単に計測してみました。

curl から

text/html: 0.00s user 0.01s system 0% cpu 5.132 total
text/xml : 0.00s user 0.01s system 0% cpu 6.716 total

管理ページ の Logs から

text/html: 4823ms 6805cpu_ms ms=4824 cpu_ms=6805 api_cpu_ms=0 cpm_usd=0.189056
text/xml : 6413ms 7291cpu_ms ms=6414 cpu_ms=7292 api_cpu_ms=0 cpm_usd=0.202572

ということで、そんなにビックリな数字でもありません。そこそこのフレームワークでの最初の SpinUp は大体このくらいではないでしょうか。

ちなみに、jsonengine の場合

/_q/msg?sort=_createdAt.desc: 3455ms 3373cpu_ms 29api_cpu_ms ms=3456 cpu_ms=3373 api_cpu_ms=29 cpm_usd=0.093822

jsonengine の方が計測条件が悪い(クエリーが発生している)はずなので、Slim3 の速度は圧倒的ですね。


ただ、jersey などのフレームワークの場合、さまざまな restful な要求に応えるために色々実装していることを勘案すると、頑張っていると言えなくもありません。結局 TPO で選択することになるのでしょう。速度優先で、すべて自前でチューンできる方は文句無しで Slim3 なんでしょうけど(笑)

Restlet の「ソースの読みやすさや GAE・GWT への対応」に心惹かれるのですが、Jersey の「POJO を簡易に Restful にするシンプルさ」にも惹かれるものがありますね。


追記: Restlet でも試してみました。

こちらの (The full source code) をそのまま本番環境にデプロイして SpinUp 速度を簡単に計測してみました。

curl から

1回目: 0.00s user 0.01s system 0% cpu 2.405 total
2回目: 0.00s user 0.00s system 0% cpu 2.736 total

管理ページ の Logs から

1回目: 2388ms 2469cpu_ms ms=2388 cpu_ms=2469 api_cpu_ms=0 cpm_usd=0.068652
2回目: 1613ms 2177cpu_ms ms=1614 cpu_ms=2178 api_cpu_ms=0 cpm_usd=0.060551

Slim3 には及びませんが、GAE 対応を謳っているだけあって、なかなか頑張っていますね。

これでいいのか javascript で HTML をエスケープ

jsonengine がシンプルな上になかなか優秀なので、お試ししてみました。

普通に CRUD する分にはまったく問題無し。快適。しかし、XSS な文字列を入力されちゃった場合の対処をどうするか。twitter でも話題に上っていたのを目にしました。

取りあえずサーバ側でエスケープするのが王道だろう、ということで修正してみました。

diff --git a/src/com/jsonengine/common/JEUtils.java b/src/com/jsonengine/common/JEUtils.java
index b2d2473..c545982 100644
--- a/src/com/jsonengine/common/JEUtils.java
+++ b/src/com/jsonengine/common/JEUtils.java
@@ -3,6 +3,7 @@ package com.jsonengine.common;
 import java.math.BigDecimal;

 import org.slim3.memcache.Memcache;
+import org.slim3.util.HtmlUtil;

 /**
  * Provides utility methods for jsonengine.
@@ -60,17 +61,18 @@ public class JEUtils {
         if (val == null) {
             return "";
         } else if (val instanceof String) {
-            return (String) val;
+            return HtmlUtil.escape((String) val);

これで、samples/bbs.html に

<script>alert("HOGE");</script>

を入力してみると、ほら大丈夫....え?!じゃない!

jQuery の使用経験はほとんどありませんので、よく分かっていないかも知れませんが、もしかしてエスケープしたタグ関連の文字列が復活しているのでしょうか? 仕方無いので、取りあえず下記サイトを参考に

Escaping text with jQuery append? - Stack Overflow

$('<div></div>').text("<br>(" + getUserName(result[i]) + ") " + result[i].msg).appendTo('#messages');

javascript 側でもエスケープを掛けることで一応解決。う〜む。クライアント側で js を改変されたらヤバいよね、これ。


追記: ブラウザから直接クエリーしてみたら、エスケープした文字列が復活していました。

/_ah/admin/datastore?kind=JEDoc では確かに

[msg:_updatedAt:13:0.1280206640719, msg:msg:&lt;script&gt;alert(&#034;FUGA&#034;);&lt;/script&gt;, msg:_createdBy:, msg:docType:msg, msg:_updatedBy:, msg:_createdAt:13:0.1280206640719]

にもかかわらず、/_q/msg では、

{"docType":"msg","msg":"<script>alert(\"FUGA\");</script>","_docId":"20HHlkxrczSB3aI3HV7QtQIzO5paI0hO","_updatedAt":1280206640719,"_createdAt":1280206640719,"_updatedBy":null,"_createdBy":null}

となっています。

ということで、原因は jQuery ではなく、jsonengine の方でしょうか? さても、どこが問題箇所なのでしょう。JEDoc の docValues あたりかなぁ...


追記の追記: かなり勘違いしていました。

indexEntries をエスケープしていても、あまり意味ありません。データ本体である Blob な docValues をどうにかしないと。


追記の追記の追記: 出口で escape の網を張ってみました。

diff --git a/src/com/jsonengine/model/JEDoc.java b/src/com/jsonengine/model/JEDoc.java
index 42a0245..9fa2669 100644
--- a/src/com/jsonengine/model/JEDoc.java
+++ b/src/com/jsonengine/model/JEDoc.java
@@ -137,7 +138,10 @@ public class JEDoc implements Serializable {
      * @return JSON document
      */
     public String encodeJSON() {
-        return JSON.encode(getDocValues());
+        return HtmlUtil.escape(JSON.encode(getDocValues())).replaceAll(
+            "&#034;",
+            "\""");
+
     }

diff --git a/src/com/jsonengine/service/query/QueryService.java b/src/com/jsonengine/service/query/QueryService.java
index 45af946..49a7fdc 100644
--- a/src/com/jsonengine/service/query/QueryService.java
+++ b/src/com/jsonengine/service/query/QueryService.java
@@ -8,6 +8,7 @@ import net.arnx.jsonic.JSON;

 import org.slim3.datastore.Datastore;
 import org.slim3.datastore.ModelQuery;
+import org.slim3.util.HtmlUtil;

 import com.jsonengine.common.JEAccessDeniedException;
 import com.jsonengine.meta.JEDocMeta;
@@ -61,12 +62,12 @@ public class QueryService {
         // return the results in JSON
-        return JSON.encode(results);
+        return HtmlUtil.escape(JSON.encode(results)).replaceAll("&#034;", "\""");
     }
 }

ただ、replaceAll とは、ちょっと無茶ぶりが過ぎるかなぁ、と。

docType からエスケープしたい対象だけ抜き出して処理する方が丁寧やも知れませんね。


追記の追記の追記の追記: 入口で escape の網を張ってみました。

やはりヤバげなデータを飲み込む、というのは何とも気持ち悪いので、入口規制を掛けてみました。

diff --git a/src/com/jsonengine/service/crud/CRUDRequest.java b/src/com/jsonengine/service/crud/CRUDRequest.java
index 021d657..05e84e1 100644
--- a/src/com/jsonengine/service/crud/CRUDRequest.java
+++ b/src/com/jsonengine/service/crud/CRUDRequest.java
@@ -5,6 +5,8 @@ import java.util.Map;

 import net.arnx.jsonic.JSON;

+import org.slim3.util.HtmlUtil;
+
 import com.jsonengine.common.JERequest;
 import com.jsonengine.model.JEDoc;

@@ -56,7 +58,18 @@ public class CRUDRequest extends JERequest {
         this.jsonDoc = jsonDoc;
         if (jsonDoc != null) {
             // decode jsonDoc and fill it into jsonMap
-            jsonMap = JSON.decode(jsonDoc, Map.class);
+            final Map<String, Object> temp = JSON.decode(jsonDoc, Map.class);
+            final String docType = temp.get("docType").toString();
+
+            if (temp.get(docType) instanceof String) {
+                final String escaped =
+                    HtmlUtil.escape(temp.get(docType).toString());
+                temp.put(docType, escaped);
+                jsonMap = temp;
+            } else {
+                jsonMap = JSON.decode(jsonDoc, Map.class);
+            }
+
             setDocId((String) jsonMap.get(JEDoc.PROP_NAME_DOCID));
         } else {
             jsonMap = null;

JSON という汎用性故に、どういうデータ形式が飛んでくるのか分かりませんので、美しくない型チェックを実施して String であれば、エスケープ処理を施すようにしてみました。

もっとイイ方法があるような気もするのですが...もう少し考えてみます。

ついに git の空のディレクトリの時代が終わる

git での空ディレクトリの扱いは、こちら (git/空のディレクトリが消えているんですけど - TOBY SOFT wiki) に掲載されている通り

一般的には、gitで空のディレクトリを追加するには、空のディレクトリをなくす、つまり例えば「空の.gitignoreファイルを置く」

で、空のディレクトリの発見は

find . -type d -empty -not -path './.git*' -exec touch {}\/.gitignore \;

ということなんでしょうが、

  • ネット上から svn でチェックアウトしたものを
  • ローカル上では git で管理する

ようなケースの場合、find 的には空ではないが、git 的には空っぽのディレクトリ、つまり「.svn」の扱いがとても面倒です。

結局、手作業で発見するのは骨が折れますので、スクリプトを書いてみました。

当該ディレクトリに移動して、下記スクリプトを実行すれば、.gitignore を生成するところまで完了してくれます。

#!/bin/sh

set -x -e -u

FIND=/usr/bin/find

if [ -x ${FIND} ]; then
    for i in `${FIND} . -type d | egrep -v '\.(svn|git)'`; do
        if [ -z "`/bin/ls $i`" ]; then
            touch $i/.gitignore
        fi;
    done

    exit 0
fi

exit 1