Web ページからのアップロードのための調査

SPECIAL

Web ページからバイナリファイルをアップロードするには…、と調べてみたときの文書です。


Web ページからのアップロード

アップロードといえば FTP ですけど、HTTP にもアップロードする機能は含まれていたと思います。

よく、管理ページを Web サイトとして作成するということがあるので、ではファイルのアップロード機能もここに含めるにはどうしたらいいのか、ということを調べてみることにします。

なお、ここでは Microsoft 社の IIS という Web サーバ上、つまりは ASP にて受け取るまでの方法を調べてみることにします。

 

なおこのページでは、実際にアップロードを行うところまでは触れていません。

だいたいどのようなデータがサーバへ送られたかを特定するまでにとどめていますので、明らかな解答がほしい方は、他のサイト様をご覧くださいませ。

 

INPUT タグの概要

まずは、存在だけは知っていた HTML の <input> タグの type="file" を調べてみることにします。

このタグを用いると、このフィールドに入力されたファイル名のデータを Web サーバへ送信することができるようです。しかも入力フィールドには、ファイルを選択するための参照ボタンが自動的に付加されるそうです。

 

また安全のため、このタグをスクリプト操作することはできないそうです。

たしかに、そうでないとサイト作成者によって任意のファイルをアップロードさせるということもできてしまいますから、妥当なセキュリティですね。

また、初期値の設定などもできない仕様になっているそうです。

 

これでクライアント側からのアップロード自体は簡単そうです。あとはサーバ側で、送られてきたデータを受け取らなくてはいけません。

これには CGI などのスクリプトが必要になるようです。

 

データの送られ方

サーバで受けるには、どのようにデータが送られてくるかを調べなくてはいけません。

調べてみると、データの形式は multipart/form-data 形式で送る必要があるようです。それに伴って <form> タグにも enctype="multipart/form-data" というオプションをつける必要があるようです。

こうして送られたデータは、Content-type: multipart/form-data; boundary=******** という感じで送られるようです。

 

MIME マルチパート形式で、boundary の値の先頭に -- をつけた文字列を使って、各項目を分断します。

それぞれの項目は、

Content-Disposition: form-data; name="xxxx"

 

という感じのヘッダーで、入力されたフィールドがわたるようです。

そのうち、ファイル情報は

Content-Disposition: form-data;name="yyyy"; filename="zzzz.txt"

Content-Type: text/plain

 

という感じでわたるようです。

データの含まれ方は一般のマルチパートドキュメント同様、ヘッダに続けて CRLF をはさんで、実際のデータとなるようです。

 

では実際にこのデータがどのように入ってくるのかを見るために、ASP にて Request.ServerVariables コレクションの値を書き出すページを用意して、ACTION をそのページに指定したフォームを作成してみました。

そして、フォームにてきとうな値を入力してみました。

 

その結果からわかることとしては、新しく CONTENT_TYPE 環境変数と HTTP_CONTENT_TYPE 環境変数が用意され、どちらとも値が "multipart/form-data; boundary=---------------------------7d215f2c704aa" という値になっていました。

この情報からだと、multipart/form-data 形式で送られたことと、マルチパートドキュメントそれぞれを分割する記号が "---------------------------7d215f2c704aa" という程度です。

他の環境変数にも、フォームに入力したようなデータは見受けられませんでした。

 

GET メソッドと POST メソッド

ヘッダ情報の格納された環境変数 (Request.ServerVariables) には、実際に入力した値らしきものは確認できませんでした。

また、POST メソッドで送信した場合には CONTENT_TYPE 環境変数が付加されましたが、GET メソッドの場合にはそれさえも付きませんでした。

 

環境変数の値にデータがなかったので、通常のフォームデータが格納される Request.Form コレクションと Request.QueryString コレクションの値を調べてみました。

その結果、GET メソッドで送信してみた場合は、Request.QueryString にて、フォームで入力されたデータを取得することはできました。ただし、<input type="file"> で指定した部分も、入力されたファイル名のみという結果です。

POST メソッドの場合は、enctype="multipart/form-data" をつけない場合には通常通り Request.Form コレクションで取得できていたのが、enctype="multipart/form-data" をつけた途端、Request.Form コレクションには、それらしい値はひとつも含まれませんでした。ただし、enctype なしの場合は、<input type="file"> で得られるのはファイル名のみです。

 

よってとりあえず、ファイルを送信するためには、POST メソッドであり、フォームのエンコード方法として "multipart/form-data" を指定してある必要がある、という察しが付きます。

 

multipart/form-data の値の取り出し

POST メソッドにて "multipart/form-data" 形式のデータを読む場合には、Request.ReadBinary メソッドを使用する必要があるようです。

これは POST メソッドで送られたデータをバイナリレベル (下位レベル) で参照する際に利用するメソッドだそうです。これを利用することで、"multipart/form-data" を、送られたままの状態で読み込むことができるようです。

ただし、この Request.BinaryRead メソッドと Request.Form コレクションとは排他的な関係にあるので、どちらか一方を呼ぶと、そのドキュメント内ではもう一方は利用できなくなるので注意が必要です。

 

では、さっそく Request.BinaryRead メソッドを使用して、フォームデータを読み込んでみることにします。

Request.BinaryRead は、引数として読み取るデータのバイト数を指定する必要があります。そして読み取ったデータは Variant 型の配列 (SafeArray 型) として値を取得します。

ためしにその戻り値を Response.BinaryWrite メソッドを使って画面に表示してみると、境界文字列 (boundary=) から始まって、各種データがマルチパート文書の書式通りに境界文字列で区切られた状態で取得することができました。

もちろん、ファイルの情報もデータ部分をしっかりと取得することができました。

そして文末は、境界文字に "--" をつけたもので終わっていました。

 

イメージ的にはこんな感じです。

-----------------------------7d23df6704aa

Content-Disposition: form-data; name="T1"

 

a

-----------------------------7d23df6704aa

Content-Disposition: form-data; name="F1";

filename="C:\temp\sample.txt"

Content-Type: text/plain

 

HTTP によるアップロードをするための

実験に使用するテキストファイルです。

-----------------------------7d23df6704aa

Content-Disposition: form-data; name="B1"

 

送信

-----------------------------7d23df6704aa--

 

T1 という名前のテキストボックスには "a" を入力してみました。また、F1 という名前のファイル名入力ボックスには C:\temp\sample.txt を指定してみました。

B1 の "送信" というのは、Submit ボタンについていた名前とその値です。

 

SafeArray 型の変数

上記の例は、受け取った SafeArray をただ純粋に Response.BinaryWrite メソッドを渡しただけですが、わざわざ配列で取得しているということは、もう少し何か気の利いたことができるはず…。

というわけで、とりあえず SafeArray について調べてみることにしました。

 

とりあえず、配列番号をつけることでデータを取り出せるようなので、まずは Request.ReadBinary で取得した配列の 0 番目の要素を取り出してみることにしました。

ところが、受け取った変数は var_bins なのですけど、var_bins(0) としてもうまくデータを取り出せないようです。

とりあえず、VBScript にある、配列の要素数を取得する関数を使ってみたところ、UBound(var_bins) なら 453 が、そして LBound(var_bins) なら 0 を取得することができました。

よって、少なくとも var_bins は配列としてちゃんと取得できているようです。

 

けれどやっぱり、var_bins(0) としてみると、var_bins の型が一致しないとのことでエラーになってしまいます。

VarType(var_bins) としてみると 8209 が、すなわち vbArray + vbByte という値が帰ってきます。では VarType(var_bins(0)) としてみると、やはり型が一致しないとのエラーです。

けれど IsArray(var_bins) とやってみれば、しっかりと True が返ってきます。

そして、For Each ... In ... のところに var_bins を用いてみると、相変わらず型が一致しないとのことでエラーが発生してしまいました。

 

う〜ん、VBScript からは、この Request.ReadBinary が返した SafeArray を読み込むことはできないのかもしれないです…。

とりあえず、LBound が 0 で UBound が 453 ということは、ちょうど配列の要素数が TotalBytes に一致するので、配列の要素 vbByte 一つ一つに1バイトずつ格納されていることが想像できますね…。

 

とりあえず、データがしっかりと渡っていることまでは把握できたので、COM など、VBScript 以外の手段でアクセスすれば容易に取り出せそうです。

なので、中途半端ですけど、ここでとりあえずの調査は終了です。

 

注意事項

ブラウザによっては正常にアップロードすることができない場合もあるようなので気に留めておくのがよさそうです。

最近のブラウザなら問題ないのかもしれませんけど、Netscape 4 と Internet Explorer 3 とかだとうまく行かないことがあるようです。

 

また、ファイルのアップロードというのは危険なものですので、データを受け取る側が特に注意する必要があります。

たとえば、実行可能ファイルをアップロードできないように注意する必要があります。任意のプログラムをアップロードして即実行、という環境が出来上がってしまうと悪用されたときに大変ですので気をつけるのがよさそうです。

また、アップロードの一環として保存するファイル名を指定できるようなシステムの場合も十分な注意が必要です。特定の保存フォルダにアップさせているつもりが、ファイル名に相対パス表記を使われたりして、予期せぬ場所へファイルを送られてしまったりするとなにかと危険です。

 

参考文献