そしてCGIへ...

MIDIを作れるJavaアプレットを作っているのだ。
前回でデータが作成できるようになったが…。

嘘書いてました。第16回でフォローしてますのでそちらもあわせてご覧ください。

セキュリティ

「なんでや!なんでやあ!サーバー側のファイルには保存できるはずちゃうかったんかあ!」
美文はうろ覚えの知識で途方にくれていた。
アプレットビューアで実行し、正しく保存されたのに気をよくし、そのままサーバにアップロードしたところで問題がおきたのだ。

java.security.AccessControlException: access denied (java.io.FilePermission http:/tgws.fromc.jp/midib/test.mid write)
	at java.security.AccessControlContext.checkPermission(Unknown Source)
	at java.security.AccessController.checkPermission(Unknown Source)
	at java.lang.SecurityManager.checkPermission(Unknown Source)
	at java.lang.SecurityManager.checkWrite(Unknown Source)
	at java.io.FileOutputStream.<init>(Unknown Source)
	at java.io.FileOutputStream.<init>(Unknown Source)
	at com.sun.media.sound.StandardMidiFileWriter.write(Unknown Source)
	at javax.sound.midi.MidiSystem.write(Unknown Source)
	at MIDIDocument.Send(MIDIDocument.java:58)
	at MIDIViewTest.mousePressed(MIDIViewTest.java:92)
	at MIDIApplet.mousePressed(MIDIApplet.java:73)
	at java.awt.Component.processMouseEvent(Unknown Source)
	at java.awt.Component.processEvent(Unknown Source)
	at java.awt.Container.processEvent(Unknown Source)
	at java.awt.Component.dispatchEventImpl(Unknown Source)
	at java.awt.Container.dispatchEventImpl(Unknown Source)
	at java.awt.Component.dispatchEvent(Unknown Source)
	at java.awt.EventQueue.dispatchEvent(Unknown Source)
	at java.awt.EventDispatchThread.pumpOneEventForHierarchy(Unknown Source)
	at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
	at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.awt.EventDispatchThread.run(Unknown Source)

予想だにしないエラーだった。
うろ覚えの知識では、セキュリティ上の問題でクライアント側のファイルには保存できないが、サーバ側のファイルには保存できるはずだった。
パーミッションは間違いないはずだ。
ということは、サーバ側のファイルには保存できるということそれ自体が間違っていたということになる。

原因を探すうち、美文はあるページを見つけた。
http://support.microsoft.com/default.aspx?scid=kb;ja;175622
これによれば、署名があり信頼されているアプレットなら保存ができるとのことだった。
裏を返せば我々のアプレットは署名が無いため信頼されていないということだ。

目的の確認と手段の再検討

「署名か…。面倒やな…。」
美文はひとりごちた。
可能なら避けて通りたい。そしてお絵描き掲示板は実際にそれを避けている。

我々の目的を今一度考えてみよう。
MIDIAPとは、お絵描き掲示板と同じ要領でMIDIを作って投稿できる掲示板システム「ミディビ」の一部として提供されるツールである。
最終的にサーバに作成したものと同じMIDIデータが置かれ、掲示板への記事の投稿と関連付けられればそれでよいのだ。
ならば、何もJavaアプレット自身が保存を担う必要はない。掲示板スクリプトにMIDIの保存もさせればよいのだ。
そういうことになれば話は早い。データの出力先をそっくりそのままCGIに向けてやればよいのだ!

とはいえCGIにデータを渡すにはそれなりのルールがあるのでデータを本当にそっくりそのまま受け渡すと受け取ったサーバがそれを理解せず期待通りに動かない。
したがって、決まった方式に則ってデータを加工して渡すことになる。

POST (実行するCGIのパス) HTTP/1.1
Host: (ホスト名):(ポート番号)
Content-Type: multipart/form-data; boundary=(区切り文字列)
Content-Length: (受け渡すデータのサイズ)
Connection: close
(空行)
(バイナリデータ)

ソケット

サーバにデータを渡すにはSocketを使うということだった。
これもやはり使い方があるようだった。

Socket socket = new Socket(ホスト名, ポート番号);

ポート番号はHTTPプロトコルを使うため80で固定してよいだろう。
ホスト名は、データを受け取るCGIのサーバのものを使うため、先立ってCGIのURLを得る必要がある。
通常、CGIはアプレットと同じか近いディレクトリに置くため、アプレットのあるディレクトリから相対パスで指定するのがよいだろう。
また、CGIのファイル名がCGI側の都合で変更になることも考え、ファイル受信用のCGIのファイル名はアプレットのパラメータとして受け取ることにした。

URL url_save = new URL(getCodeBase()+getParameter("url_save"));

getCodeBase()でアプレットの置いてあるディレクトリのURLを得ている。直、getDocumentBase()はアプレットを貼り付けてあるページのファイルのURLであるためこの場合は使えない。
ホスト名とパスはそれぞれgetHost()とgetPath()で得られるのでhostとpathに格納して後で使うのだ。

String host = applet.url_save.getHost();
String path = applet.url_save.getPath();
socket = new Socket(host,80);

実際にデータを送るにはsocket.getOutputStream()で得られるOutputStreamを通して行うが、文字列の送信に向かないため実際はさらにPrintWriterを通すことになる。

送信データを用意する

「こっからどうすりゃええねん…」
美文はまたも途方にくれていた。
Sequence.writeの最後の引数をFileからOutputStreamに変更することで出力先を自由に変更することができるが、今回のケースに合うクラスが見つか(中略)った。
ByteArrayOutputStreamである。
今回のケースは送信前に予め送信サイズを把握しておく必要があったため一時的にByte配列に保管しておく必要があり、このByteArrayOutputStreamは正にそれ専用のクラスだったのだ。
更に誂え向きにこのクラスには、自身の持つデータをそっくりそのまま他のOutputStream、すなわち今回のsocket.getOutputStream()に対して書き込むwriteToメソッドまで用意されていた。
最早これを使わぬ手はないと美文は考えた。

ByteArrayOutputStream stream = new ByteArrayOutputStream();
MidiSystem.write(seq,1,stream);
PrintWriter print = new PrintWriter(socket.getOutputStream());
print.println("POST "+path+" HTTP/1.1");
print.println("Host: "+host+":80");
print.println("Content-Type: multipart/form-data; boundary=__MIDIAP_BOUNDARY__");
print.println("Content-Length: "+stream.size());
print.println("Connection: close");
print.println();
print.flush();
stream.writeTo(socket.getOutputStream());
socket.getOutputStream().flush();
socket.close();

boundaryは絶対にデータ本体と衝突しない文字列だが、元々MIDIは文字以外のデータが非常に多く含むため、深く考えずに文字列を選んだ。
flush()はOutputStreamに書き込んだデータを反映する処理なのでおそらく必須であろう。あって困ることはない。

受信CGI

後残りで必要なのはアプレットの送ってくれたデータを解釈し、ファイルに保存するCGIである。
何もCGIに限る必要はないが使い慣れているのでこれにしたのだ。
お絵描き掲示板のように多くのデータを受け取るわけではないので唯受信したデータをそのままファイルに書き出すだけだ。

#!/usr/local/bin/perl

# postされたデータを読み込む
binmode STDIN;
read(STDIN,$data,$ENV{'CONTENT_LENGTH'});
print "Content-type: text/html\n\nok\n";
open(F,"> test.mid");
binmode F;
print F $data;
close(F);

要点

例のアレ。
そしてCGIへ...

続く