ApacheのSNIはAndroidブラウザで使えない

Apacheの2.2.12からSNIが搭載され、1つのサーバでVirtualHost別のSSL証明書が使用出来るようになりました。
http://wiki.apache.org/httpd/NameBasedSSLVHostsWithSNI
これは大変よろしくて、ブラウザのサポート状況もWindowsXPを除く主要ブラウザで利用可能です。iPhoneブラウザも利用可能でした。
ただし、Androidではどうも利用出来ないようなのです。それも古いバージョンではなく、現時点では最新のGingerbread(2.3)を利用してもです。
AndroidのWebは癖が強く、他の環境では必要ない中間証明書が必要であったりと、従来のガラケー開発と同様の確認は必須になりそうです。Android端末でのWebの利用は無視出来なくなってきてるので気をつけましょう。

ピンチイン、アウト可能なImageVIewを作ってみた

Android標準のビューはiPhoneのようなピンチイン/アウトをサポートしてません。実現したければ自前で実装する必要があります。
Web上を探してみたけどしっくりくる物が無かったので作ってみました。簡単そうだけど考える事が多くて大変です。
ソースとサンプルアプリを公開しているので良かったら見てみてください。シンプルかつ無駄の多い実装ですが簡単なアプリには使えると思います。
http://code.google.com/p/scale-imageview-android/

WebViewで動的に画像を設定する方法

WebViewに画像を表示する場合、静的に決まっている画像であればassetsに放り込んでfile:///android_asset/のURLでimgタグを作れば良いのですが、編集した画像だったりをJavaから設定したい場合にはdata:imageスキームを使う事で実現出来ます。

<img src="data:image/png;base64,%s">

こんなHTMLを用意しておき
読み込んだ画像をバイト配列にしてBase64して流し込みます。

InputStream is = [Your Image]
byte[] b = new byte[is.available()];
is.read(b);
String image64 = Base64.encodeToString(b, Base64.DEFAULT);
String html = String.format(baseHtml, image64);
webview.loadData(html, "test/html", "utf-8");

LionでMySQLのコンパイルに失敗する

MacBook Airを購入したので早速開発環境を整えているとMySQLのインストールでハマりました。ソースコードからmakeしたい派なのですが、いつも通り

cmake . -DCMAKE_INSTALL_PREFIX=/Users/matabii/apps/mysql5.5
make

するとエラーになります。

Scanning dependencies of target GenError
[ 28%] Generating ../include/mysqld_error.h, ../sql/share/english/errmsg.sys
/bin/sh: line 1: 21749 Segmentation fault: 11  ./comp_err --charset=/Users/matabii/apps/src/mysql-5.5.15/sql/share/charsets --out-dir=/Users/matabii/apps/src/mysql-5.5.15/sql/share/ --header_file=/Users/matabii/apps/src/mysql-5.5.15/include/mysqld_error.h --name_file=/Users/matabii/apps/src/mysql-5.5.15/include/mysqld_ername.h --state_file=/Users/matabii/apps/src/mysql-5.5.15/include/sql_state.h --in_file=/Users/matabii/apps/src/mysql-5.5.15/sql/share/errmsg-utf8.txt
make[2]: *** [include/mysqld_error.h] Error 139
make[1]: *** [extra/CMakeFiles/GenError.dir/all] Error 2
make: *** [all] Error 2

情報を探してみると以下を発見
http://lists.mysql.com/commits/140413
ソースを変更してやり直すと、さっきのエラーは出なかったけどpthread_init()消したからかatomic系の所でエラーが出ました。

Scanning dependencies of target my_atomic-t
[ 94%] Building C object unittest/mysys/CMakeFiles/my_atomic-t.dir/my_atomic-t.c.o
/var/folders/f9/n1wybs0d2xz19678ln60xsl80000gn/T//ccD2v5wp.s:596:suffix or operands invalid for `add'
make[2]: *** [unittest/mysys/CMakeFiles/my_atomic-t.dir/my_atomic-t.c.o] Error 1
make[1]: *** [unittest/mysys/CMakeFiles/my_atomic-t.dir/all] Error 2
make: *** [all] Error 2

Lionのpthreadライブラリを使えていないという情報もあったので、xcodeでビルドすると上手く行きました。

cmake . -G "Xcode" -DCMAKE_INSTALL_PREFIX=/Users/matabii/apps/mysql5.5
xcodebuild -target install

In-app Billingでsignatureの検証をサーバサイドで行う

Androidのアプリ内課金を安全に使うためには何らかの防御策を取らないといけないのですが、その方法の一つとしてsignatureの検証があります。
購入完了後にMarketから購入情報がjsonで送信されてきます。

{"nonce":2102159826496562602,"orders":[{"notificationId":"2764732181918735088",
"orderId":"259065433347272","packageName":"com.matabii.test.billing1",
"productId":"product1","purchaseTime":1304573886000,"purchaseState":0}]}

これとセットで、jsonのsignatureも送られてきます。

YG7sJWdElGYBTkmRimrek0tZ7Y7WISxH8hAZwrpu/i4bfsR22PQ5ODjWHF9Z9RxmktMpQY/y6IxV
yaIs4lR1v5GNCXt3PHaIPo2lSjrwlHmyczs+YiD+DdbG6rHCoVuag7le5+5APLCGha9cpikFAZCX6D
YVY2kD1w7svyU6HxdsPAtU5HGdMEdK+CIsfDg+ALWIJ/rSr9WMvT/iMYjiCRhYMuy
c6sCamS9Ag7kpZqtHYbCCC0S9FdM5VMCzbY/BxA+4LYdq/cJL0+dVIElfWnIk/xzwBQ60IAicZX9D
S4zYZc9WnvgQhjTjKkj5l4luwy825jyAInhXagdDCN3qtg==

このsignatureはAndroid Marketにて、パブリッシャーの秘密鍵を使って生成されています。秘密鍵はアプリの作者であっても知ることが出来ませんが、対応する公開鍵はパブリッシャーサイトにログインすれば分かります。
http://developer.android.com/images/billing_public_key.png

BillingのサンプルアプリにはJavaを使った検証コードが書いてありますが、アプリの中に検証コードと公開鍵を埋め込むのはセキュリティ上よろしくなく、可能であればリモートサーバを使って検証するべし!と公式にも書かれているので、サーバサイドでPHP+opensslを使って検証してみます。

opensslで検証する

まずはPHPを使わず、Linuxサーバでopensslを使って検証してみます。opensslでやり方が分かれば、PerlPythonを使うにしても同じだと思うので。

公開鍵を利用出来る形式にする

パブリッシャーサイトで得られる公開鍵ですが、これはBase64 エンコードの RSA 公開鍵です。と書かれています。暗号化について人に説明出来るほどの知識は無いので省略しますが、鍵のフォーマットにも色々あって、opensslが理解出来るPEMかDERに変換します。
まずは公開鍵をコピペしてテキストファイルとして保存します。空白や改行コードが混じらないように注意して下さい。どうしても上手くいかない場合は、バイナリエディタで確認してみましょう。ここでは「public」というファイル名で保存したとします。
base64エンコードされてるということなので、base64デコードをかけます。

base64 -d public > public.der

base64デコードすれば、DER形式として使える公開鍵になります。これをPEM形式にするにはこのpublic.derを利用します。

openssl rsa -inform DER -outform PEM -pubin -in public.der -out public.pem

これでPEM形式の公開鍵が得られます。opensslコマンドは難解ですね・・・

signatureもデコードする

Jsonについてきたsignatureもbase64エンコードされているので、デコードしておきます。signatureというファイル名でテキスト保存しているとして

base64 -d signature > signature.bin
検証する

これで役者が揃いました。signature.binとpublic.derを使ってMarketから送信されてきたjsonを検証してみます。jsonについては「signeddata」というファイル名で保存されているとします。
暗号化の方法はRSA公開鍵方式ですが、暗号化についてはsha1で行なっているようです。
なので検証用のopensslコマンドは、

openssl dgst -sha1 -keyform DER -verify public.der -signature signature.bin  signeddata 

「Verified OK」と表示されると成功です。試しにsigneddataのjsonを改ざんしてみると「Verification Failure」になりました。
ちなみにPEM形式の鍵、public.pemを使うには以下のようにします。

openssl dgst -d -sha1 -keyform PEM -verify public.pem -signature signature.bin  signeddata

pem鍵を用意する理由ですが、PHPのopenssl関数がPEM形式しか受け付けてくれなかったからです。

PHPで検証する

PHPは--with-openssl付きでコンパイルしていないといけません。

<?php
$data = file_get_contents("signeddata");
$signature = file_get_contents("signature.bin");
$pubkey = file_get_contents("public.pem");
$pubkeyid = openssl_get_publickey($pubkey);

$ok = openssl_verify($data, $signature, $pubkeyid);
var_dump($ok);

In-app Billingの問題点

正式に公開されたものの良い噂を聞かないですね。色々触ってみて発見した問題点を思いつく限り書いてみます。
2011年4月24日時点の情報です。進化が早いので最新情報は公式ドキュメントを参照して下さい。

情報が少ない

http://developer.android.com/guide/market/billing/index.html
ぺらっぺらの情報しかない上にデベロッパー向けで難解です。

デベロッパーコンソールが貧弱

これがアプリ内課金の管理画面です。

Googleなのに検索が出来ないです。ダウンロード数の把握も出来ません。一括ダウンロードやアップロードは出来ないし、数が増えると管理が大変でしょうね。今んとこ数十件くらいが限界でしょうか。ってか1つのアプリに登録出来るアイテム数のリミットっていくつなんでしょう。

機種によって挙動が違う

Androidに付きまとう厄介な問題ですが、billingでも健在です。
http://groups.google.com/group/android-group-japan/browse_thread/thread/8b9789d2e83421ef/
機種別に個別対応が必要になりそうな予感です。

テスト環境が無い

eclipseで実行してもまともに動かないですよ。ちゃんと署名してマーケットにアップロードして、そこからDLしたものをインストールして、クレジットカードを登録してCheckoutで購入して下さいね。(マーケットからDLしないと動かないはバグ?)
あと、これは有料アプリの販売も同じですが、販売アカウントのCheckoutと端末に登録したGmailアカウントが同じ場合は上手く動かないです。Androidに紐付いているGmailアカウントのサインアウトは不可能なので、「端末を初期化して」別のGmailアカウントでサインインしてね。って公式ドキュメントに書いてあって愕然としました。複数のアカウントでテストする時は何回も端末の初期化を行うのですね。1台しか持ってない人はもう1台買えば良いと思います。その端末がアプリ内課金をサポートしているはずのバージョンでも、ちゃんと動く保証は無いですけど。

外部のサーバから購入情報の取得が出来ない

認証はアプリでやったとしても、決済情報のやりとりはサーバ間でやるのが常識(だと思ってました。)
しかしアプリ以外からのアクセスを受け付けてくれないので、例えばポイントをアプリ内課金で販売する場合、ポイント管理サーバはアプリから送信された情報を信用するしかなく、何かしらの認証の仕組みを自前で用意してあげなくちゃいけません。

アイテムID、タイトル、説明、価格など、デベロッパーコンソールで登録した情報をアプリから取得出来ない

アプリ内課金は、購入したいアイテムIDを指定して処理を呼び出すことで開始します。
が、アプリに登録されているアイテムIDをサーバから取得することは出来ません。タイトルや説明や価格もです。
つまり、販売するアイテム情報は全てアプリに埋め込まなければいけません。アイテムを追加したい場合はアプリの更新も必要になりますね。またはコンテンツ管理のサーバを用意することです。

実装の難易度が高い

nonceとかdeveloperPayloadとか説明を読んだだけでは理解出来ない項目が出てきたり、簡単にクラック出来ちゃうのでソースの難読化のProguardを公式に勧めてきたり、ドキュメント読むと頭が痛くなる上にサンプルアプリを動かすだけでお腹いっぱいになっちゃいます。
トライ&エラーは仕方ないとしても、端末によって挙動がおかしかったり、マーケットにアップしたものを公開してDLしないと動かない機能があったり、テスト購入するにはクレジットカードが必要だしCheckoutで決済されたのをキャンセルしたり、場合によっては端末の初期化をしたりと非常に面倒です。

セキュリティを担保するのが難しい

実装の難易度が高いので、とりあえず動く状態で満足しちゃいますが、それでリリースするのは危険です。例えばnonceやsignatureの検証を一切行っていないとすると、3分でクラックされて偽の購入完了通知を送られることになります。この購入情報をサーバに送ってポイント追加してたりすると、ポイントの無限増殖されちゃいますね。
また、アプリ自体が有料であれば買わないといけませんが、無料でDLさせて追加ステージをアプリ内課金で販売してたとすると、敷居が低い分カジュアルハッカーに狙われる可能性が増えそうです。

以上!

まだ実用レベルじゃ無いですよね・・・。特にテスト環境が無いのが致命的です。端末の初期化て・・・とりあえず動くことは動くので、腕に自信がある人は挑戦しがいがあると思いますが、もう少し待てば状況も改善されるのでしょうか。One Passの状況も気になりますし。

コンテンツ配信やセキュリティでリモートサーバの利用がほぼ必須になりそうなので、そういったサービスが出てくることにも期待ですね。

Djangoでmodels.pyの分割

最近Pythonフレームワーク、Djangoを触ってます。
そこそこ軽量で、テスト用Webサーバやタスクなんか揃ってて気に入っているのですが、使ってると不満も出てきます。その1つがMVCのモデルを表すmodels.pyです。
これ、普通に作ってるとアプリ毎にmodels.pyという名前で用意しないといけないので、沢山のモデルがある場合に巨大なファイルになってしまうし、アプリがいくつかあると、どこに何のモデル置いてたかわからなくなってしまうのです。
希望としては、テーブル名+.pyとして、プロジェクトに対して1箇所のディレクトリにまとめたいです。まぁそれは出来るのですが、manage.pyタスクのsqlやsyncdbも有効に使いたいです。
これが良い方法かはまだ分かりませんが、何とか出来たのでその方法です。

ディレクトリ構造です。modelsディレクトリを作成して、モデルは全てそこに置きます。
+ mysite/
  + models/
  |- __init__.py
  |- models.py # 空ファイル
  |- table1.py
  |- table2.py
  |...
  .
各モデルファイルはこんな感じです。
from django.db import models 
class Table1Model(models.Model): 
    title = models.CharField(max_length=50)
    class Meta:
        app_label = "models"
__init__.pyに読み込むモデルを書いておきます。
table1 import Table1Model
table2 import Table2Model
...

あとはsettints.pyのINSTALLED_APPSに、modelsを追加しておきます。manage.pyのsqlSQL作成、syncdbでテーブルも作成されます。モデルのファイルのapp_labelがテーブル名のプレフィックスに付きます。上の例ならmodels_table1modelテーブルが作成されます。
modelsアプリケーションがあるとDjangoに認識させてるわけで、空でも良いのでmodels.pyのファイルを用意しなくちゃいけないのがバッドノウハウです。マイグレーションもしてくれないし、manage.pyのタスクを作っちゃうってのもありかも。