シェルスクリプトでCGIスクリプトを作る方法を解説する。CGI (Common Gateway Interface)は、単純に説明するとブラウザからWebサーバに置いてあるプログラムを起動するための仕様。CGIという言葉はインターフェースを指すので、CGIで動く(動かされる)プログラムのことは、CGIプログラムと言ったりCGIスクリプトと呼んだりする。スクリプト言語で書いた場合はCGIスクリプトとすればよい。
CGIプログラムはどんなプログラミング言語で作ってもかまわない。C言語で書いてもよい。この領域では軽量言語(LL言語)で書かれることが多い。代表的なものにPHP、Python、Perl、Rubyなどがある。このレベルで動作するプログラミング言語としてシェルスクリプトを利用するという話を本稿では取り上げる。
CGIプログラムを組む場合、そもそもPHPやPerl、RubyなどCGIプログラムとして利用することを前提とした、または実際に使われることが多いプログラミング言語が存在するため、最初の選定の段階でこれらプログラミング言語を選択する傾向がある。しかし、CGIの仕組みそのものはシンプルで、これらスクリプト言語に限定する必要はない。
動作にはbash(1)およびApache HTTP Serverが動くUNIX系の環境を利用する。ここではMac OS Xで使用する場合を取り上げるので、FreeBSDやLinuxで実行する場合には適宜読み替えてほしい。
Mac OS XにはApache HTTP Serverがはじめから導入されている。リスト1のようにコマンドを打つと、Apache HTTP Serverが起動する。
$ sudo -s # apachectl start org.apache.httpd: Already loaded # ps cax | grep httpd 16023 ?? Ss 0:00.15 httpd 16024 ?? S 0:00.00 httpd $
HTTP Serverの動作確認は次のようにコマンドを実行する。
$ curl http://localhost <html><body><h1>It works!</h1></body></html> $
次にリスト3のようにCGIプログラムを置くディレクトリを確認する。CGIプログラムはApache HTTP Serverではcgi-binというディレクトリにおくことが多い。
$ apachectl -V | grep conf -D SERVER_CONFIG_FILE="/private/etc/apache2/httpd.conf" $
$ cat /private/etc/apache2/httpd.conf | grep cgi-bin ScriptAliasMatch ^/cgi-bin/((?!(?i:webobjects)).*$) "/Library/WebServer/CGI-Executables/$1" #ErrorDocument 404 "/cgi-bin/missing_handler.pl" $
/Library/WebServer/CGI-Executables/に配置する設定になっていることがわかる。作業の利便性のために次のようなシンボリックリンクを作成する。
$ ln -s /Library/WebServer/CGI-Executables/ ./cgi-bin $ cd cgi-bin $ sudo chown ueda:wheel ./
/tmp/の下にhogeというファイルを作り所有者をApache HTTP Serverの実行ユーザに設定しておく。Apache HTTP Serverの実行ユーザおよびグループはリスト5のように調査できる。
$ grep ^User /private/etc/apache2/httpd.conf User _www $ grep ^Group /private/etc/apache2/httpd.conf Group _www $
リスト6のようにhogeを設置する。
$ touch /tmp/hoge $ sudo chown _www:_www /tmp/hoge
次にリスト7のようにrm(1)コマンドを~/cgi-binの下に置く。拡張子は.cgiにしておく。
$ cp /bin/rm ~/cgi-bin/rm.cgi
このrm.cgiをブラウザで呼び出す。ブラウザのアドレス欄にhttp://localhost/cgi-bin/rm.cgi?/tmp/hoge と書く。
ブラウザには次のようなInternal Server Errorが表示される。
/tmp/hogeはリスト8のように消えている。
$ ls /tmp/hoge ls: /tmp/hoge: No such file or directory $
ブラウザからhttp://localhost/cgi-bin/rm.cgi?/tmp/hogeにアクセスすることで、サーバの~/cgi-bin/の下のrm.cgiのオプションに/tmp/hogeを渡して/tmp/hogeを消したということになる。ssh(1)でリモートのサーバに対し次のように実行するようなものといえる。
$ ssh <ホスト> '~/cgi-bin/rm.cgi /tmp/hoge'
ブラウザに文字例を表示するための最小限のCGIスクリプトをリスト9に示す。
$ cat smallest.cgi #!/bin/sh -xv echo "Content-Type: text/html" echo "" echo 魚眼perlスクリプト $ chmod +x smallest.cgi $
実行すると次のようになる。ブラウザから実行すれば文字列はブラウザに表示される。
$ ./smallest.cgi 2> /dev/null Content-Type: text/html 魚眼perlスクリプト $
Content-Type-type: text/htmlはHTTPプロトコルで定められたHTTPヘッダ。さきほどのrm.cgiでブラウザにエラーが出たのはHTTPヘッダをrm.cgiが出力していないためだ。ブラウザとApache HTTP ServerはHTTPプロトコルでしゃべっているので、CGIプログラムもHTTPプロトコルでしゃべる必要がある。
ヘッダの次のecho ""はヘッダと中身を区切る空白行を出すために書いてある。ヘッダの前には余計なものを出してはいけないので、たとえばリスト11のようなCGIスクリプトをブラウザから呼び出すとブラウザにエラーが表示されてしまう。
$ cat dame.cgi #!/bin/sh -xv echo huh? echo "Content-Type: text/html" echo "" echo 湾岸pythonスクリプト $ curl http://localhost/cgi-bin/dame.cgi 2> /dev/null | head -n 3 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>500 Internal Server Error</title> $
シェルスクリプトはただ標準出力に文字列を出力しているだけだ。ブラウザやWebサーバに何か特別なことをしているわけではない。シェルスクリプトから適切なHTTPプロトコルを標準出力へ書き出すだけで事は完了する。
シバン(#!/bin/sh)の行にログを出力する-vxというオプションをつけている。このログはApache HTTP Serverのエラーログに出力される。
$ cat /private/var/log/apache2/error_log (略) [Tue Apr 23 21:46:14 2013] [error] [client ::1] #!/bin/bash -xv [Tue Apr 23 21:46:14 2013] [error] [client ::1] [Tue Apr 23 21:46:14 2013] [error] [client ::1] echo "Content-Type: text/html" [Tue Apr 23 21:46:14 2013] [error] [client ::1] + echo 'Content-Type: text/html' [Tue Apr 23 21:46:14 2013] [error] [client ::1] echo "" [Tue Apr 23 21:46:14 2013] [error] [client ::1] + echo '' [Tue Apr 23 21:46:14 2013] [error] [client ::1] echo \xe9\xad\x9a...(略) $
ターミナルからブラウザに文字などを送り込むCGIプログラムを作ってみる。リスト13のようなシェルスクリプトを作る。
$ cat notify.cgi #!/bin/sh mkfifo /tmp/pipe chmod a+w /tmp/pipe echo "Content-Type: text/html" echo "" cat /tmp/pipe rm /tmp/pipe $
4行目のmkfifo(1)というコマンドは「名前つきパイプ」というファイルを作るコマンド。たとえば次のようなコマンドがあったとする。
$ echo hoge | cat
この処理を名前付きパイプで書くとリスト14のようになる。
$ cat /tmp/pipe
$ echo hoge > /tmp/pipe $
端末1のcat(1)は/tmp/pipeにデータが流れてくるまで止まった状態になり、端末2でecho hogeが実行されたら 端末1がhogeと出力するようになる。echo hoge が終わると、 cat(1)も終わる。この動作はパイプと同じだ。/tmp/pipeはrm(1)で消さない限り残る。
notify.cgiをブラウザから呼び出す。CGIスクリプトはcat /tmp/pipeで一旦止まるので、ブラウザは待ちの状態になる。
次に端末からリスト15のように打ってみる。
$ echo '<script>alert("no more XSS!!")</script>' > /tmp/pipe $
図3のようにアラートが出たら成功だ。
notify.cgiをリスト2のように書き換えてもう一度実行する。
$ cat notify2.cgi #!/bin/sh mkfifo /tmp/pipe chmod a+w /tmp/pipe echo "Content-Type: text/plain" echo "" cat /tmp/pipe rm /tmp/pipe $
今度はブラウザに「<script>alert("no more XSS")</script>」と文字列が表示されたと思う。
次はファイルのダウンロードをやってみよう。たとえばリスト17のようなCGIプログラムを作成する。
$ cat download_xlsx.cgi #!/bin/sh -xv FILE=/tmp/book1.xlsx LENGTH=$(wc -c $FILE | awk '{print $1}') echo "Content-Type: application/octet-stream" echo 'Content-Disposition: attachment; filename="hoge.xlsx"' echo "Content-Length: $LENGTH" echo cat $FILE $
7行目のapplication/octet-streamは「バイナリを送り込む」という宣言、8行目は「hoge.xlsxという名前で保存すること」、9行目は変数LENGTHに書いてあるサイズのデータを出力する、という意味になる。
そして実際にファイルをブラウザに向けて発射するのには11行目のcat(1)コマンドになる。cat(1)はテキストもバイナリも区別しない。
ファイルはあらゆるものがダウンロードさせることができるが、ヘッダについては適宜に変化させる。たとえばmpegファイルをブラウザに直接見せたいのならリスト18のように書く。
$ cat download_movie.cgi #!/bin/sh FILE=/tmp/japanopen2006_keeper.mpeg LENGTH=$(wc -c $FILE | awk '{print $1}') echo "Content-Type: video/mpeg" echo "Content-Length: $LENGTH" echo cat $FILE $
ヘッダにContent-Disposition: attachment; filename="hoge.mpeg"'を加えると、ファイルを再生するかファイルに保存するか聞かれたり、再生されずにファイルに保存されたりする。
シェルスクリプトでCGIスクリプトを書いた。本稿の内容で一番重要なのは、Apache HTTP Serverを経由してブラウザにコンテンツを送るときには標準出力を使うということだろう。CGIの仕組みはとてもシンプルなもので、CGIプログラムもそれに合わせてシンプルに記述するだけでよい。
Software Design 2013年7月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【19】CGIスクリプトを作る(1)―Webサーバへのデータは標準出力で渡す」より加筆修正後転載