UECジャーナル

開眼☆シェルスクリプト CGIスクリプトを作る(1)―Webサーバへのデータは標準出力で渡す

シェルスクリプトでCGIスクリプトを作る方法を解説する。CGI (Common Gateway Interface)は、単純に説明するとブラウザからWebサーバに置いてあるプログラムを起動するための仕様。CGIという言葉はインターフェースを指すので、CGIで動く(動かされる)プログラムのことは、CGIプログラムと言ったりCGIスクリプトと呼んだりする。スクリプト言語で書いた場合はCGIスクリプトとすればよい。

CGIプログラムはどんなプログラミング言語で作ってもかまわない。C言語で書いてもよい。この領域では軽量言語(LL言語)で書かれることが多い。代表的なものにPHP、Python、Perl、Rubyなどがある。このレベルで動作するプログラミング言語としてシェルスクリプトを利用するという話を本稿では取り上げる。

CGIプログラムを組む場合、そもそもPHPやPerl、RubyなどCGIプログラムとして利用することを前提とした、または実際に使われることが多いプログラミング言語が存在するため、最初の選定の段階でこれらプログラミング言語を選択する傾向がある。しかし、CGIの仕組みそのものはシンプルで、これらスクリプト言語に限定する必要はない。

Apache HTTP Serverを準備

動作にはbash(1)およびApache HTTP Serverが動くUNIX系の環境を利用する。ここではMac OS Xで使用する場合を取り上げるので、FreeBSDやLinuxで実行する場合には適宜読み替えてほしい。

Mac OS XにはApache HTTP Serverがはじめから導入されている。リスト1のようにコマンドを打つと、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の動作確認は次のようにコマンドを実行する。

リスト2: curlで動作確認

$ curl http://localhost
<html><body><h2>It works!</h2></body></html>
$ 

次にリスト3のようにCGIプログラムを置くディレクトリを確認する。CGIプログラムはApache HTTP Serverではcgi-binというディレクトリにおくことが多い。

リスト3: 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/に配置する設定になっていることがわかる。作業の利便性のために次のようなシンボリックリンクを作成する。

リスト4: ホームから簡単にアクセスできるようにする

$ ln -s /Library/WebServer/CGI-Executables/ ./cgi-bin
$ cd cgi-bin
$ sudo chown ueda:wheel ./

CGIプログラムとは

/tmp/の下にhogeというファイルを作り所有者をApache HTTP Serverの実行ユーザに設定しておく。Apache HTTP Serverの実行ユーザおよびグループはリスト5のように調査できる。

リスト5: Apache HTTP Serverの動作するユーザ、グループを調査

$ grep ^User /private/etc/apache2/httpd.conf
User _www
$ grep ^Group /private/etc/apache2/httpd.conf
Group _www
$ 

リスト6のようにhogeを設置する。

リスト6: ファイルを置いてApache HTTP Serverから操作できるように所有者変更

$ touch /tmp/hoge
$ sudo chown _www:_www /tmp/hoge 

次にリスト7のようにrm(1)コマンドを~/cgi-binの下に置く。拡張子は.cgiにしておく。

リスト7: rm(1)コマンドに拡張子をつけてcgi-binに置く

$ cp /bin/rm ~/cgi-bin/rm.cgi

このrm.cgiをブラウザで呼び出す。ブラウザのアドレス欄にhttp://localhost/cgi-bin/rm.cgi?/tmp/hoge と書く。

ブラウザには次のようなInternal Server Errorが表示される。

図1: rm.cgiを実行した結果

/tmp/hogeはリスト8のように消えている。

リスト8: /tmp/hogeが消える

$ 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シェルスクリプトを書く

ブラウザに文字例を表示するための最小限のCGIスクリプトをリスト9に示す。

リスト9: 最小限のCGIスクリプト

$ cat smallest.cgi 
#!/bin/sh -xv

echo "Content-Type: text/html"
echo ""
echo 魚眼perlスクリプト
$ chmod +x smallest.cgi 
$ 

実行すると次のようになる。ブラウザから実行すれば文字列はブラウザに表示される。

リスト10: 端末からCGIスクリプトを実行

$ ./smallest.cgi 2> /dev/null
Content-Type: text/html

魚眼perlスクリプト
$ 

図2: smallest.cgiを実行した結果

Content-Type-type: text/htmlはHTTPプロトコルで定められたHTTPヘッダ。さきほどのrm.cgiでブラウザにエラーが出たのはHTTPヘッダをrm.cgiが出力していないためだ。ブラウザとApache HTTP ServerはHTTPプロトコルでしゃべっているので、CGIプログラムもHTTPプロトコルでしゃべる必要がある。

ヘッダの次のecho ""はヘッダと中身を区切る空白行を出すために書いてある。ヘッダの前には余計なものを出してはいけないので、たとえばリスト11のようなCGIスクリプトをブラウザから呼び出すとブラウザにエラーが表示されてしまう。

リスト11: HTTPヘッダの前に何か出力するとエラーになる

$ 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のエラーログに出力される。

リスト12: error_logにCGIスクリプトの標準エラー出力がたまる

$ 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を作ってみよう

ターミナルからブラウザに文字などを送り込むCGIプログラムを作ってみる。リスト13のようなシェルスクリプトを作る。

リスト13: notify.cgi

$ 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のようになる。

リスト14: 名前付きパイプを使う

端末1

$ cat /tmp/pipe
      

端末2

$ 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のように打ってみる。

リスト15: 送り込む文字列

$ echo '<script>alert("no more XSS!!")</script>' > /tmp/pipe 
$ 

図3のようにアラートが出たら成功だ。

図3: ブラウザでアラートが表示される

notify.cgiをリスト2のように書き換えてもう一度実行する。

リスト16: notify2.cgi

$ 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プログラムを作成する。

リスト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のように書く。

リスト18: mpegファイルを見せるためのCGIスクリプト

$ 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
$ 

図4: 動画を表示させることもできる

ヘッダにContent-Disposition: attachment; filename="hoge.mpeg"'を加えると、ファイルを再生するかファイルに保存するか聞かれたり、再生されずにファイルに保存されたりする。

おわりに

シェルスクリプトでCGIスクリプトを書いた。本稿の内容で一番重要なのは、Apache HTTP Serverを経由してブラウザにコンテンツを送るときには標準出力を使うということだろう。CGIの仕組みはとてもシンプルなもので、CGIプログラムもそれに合わせてシンプルに記述するだけでよい。

Software Design 2013年7月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【19】CGIスクリプトを作る(1)―Webサーバへのデータは標準出力で渡す」より加筆修正後転載

Last modified: 2014-01-13 00:00:00