シェルスクリプトでCGIチャットを書いてみました。

CGIは、標準入出力と環境変数を用いて動的コンテンツを生成する仕組みなので、これらを扱うことができるプログラムだったら記述言語は問いません。CGIと言えば「CGI/Perl」の組み合わせだけが突出して有名ですが、CGI/Cだって世の中にはたくさんあります。 さらに極端な事を言えば、CGI/awkだってCGI/shだって不可能ではありません。

ただし、CGI/shといったものは少なくとも私の知る限りでは、現在推奨はされていません。 Webインタフェイスとして動作するためのセキュリティを高めることに対する煩雑さであったり、大量のリクエストを効率的に捌くための本質的な性能であったり様々でしょうが、わざわざシェルスクリプトを引っ張り出してこなくても、Perl や Ruby、Python と言った気の利いた高級言語が使える環境が整っていることがほとんどでしょう。

じゃあ何故 CGI 用途にシェルスクリプトを使うかっていうと、限定された趣味の世界で、趣味目的で書く、というところが現実的なんじゃないかなって思っています。また、「(高級言語を使わなくても)シェルスクリプトだっていろいろ出来るよ!」という気持ちもあるかもしれません。(PQI Air Card のようなめちゃくちゃ小さい Linux システムにもシェルは搭載されているので、CGI/sh で簡単なウィキシステムを書いて遊んでみたりしました。)

bash.CGI

CGI として動作させるために重要なのが、リクエストの解析です。必要なものは環境変数に詰まっていますし、POST送信の内容は標準入力を読めば自分で何とでも出来ますが、シンプルかつしっかり動くbash.CGIというコードのサンプルがあるので、それをモジュールとして使いたいと思います。このコードは、CGI/Perl における CGI.pm みたいなもので、GETによるURLに付随してくるパラメータやPOST送信されたデータをシェル変数に置き換えてくれるので、いろいろ捗ります。

cgi_get_POST_vars()cgi_decodevar()cgi_getvars() の3つの関数定義と、呼び出しの大元である cgi_getvars BOTH ALL コマンドを貼っつけたスクリプトファイルを bashcgi.sh として保存しておきます。

bootstrap

最近のナウいユーザインタフェイスをちゃっちゃか実現するために Twitter の bootstrap を使っています。今回はサーバーのルートに /css や /js といったディレクトリが展開されるよう配置しています。

shellchat

さていよいよ本題。シェルスクリプトによる CGI チャットのソースコードは次のようになりました。

#!/bin/bash

. bashcgi.sh

title='shellchat!'
logfile='./log.txt'
linenb='15'

if [ -n "$nick" -a -n "$msg" ]; then
  msg=$(echo "$msg" | sed 's/</\&lt;/g' | sed 's/\(http:\/\/[^  ]*\)/<a href=\1 target=_blank>\1<\/a>/g')
  nick=$(echo "$nick" | sed 's/</\&lt;/g')
  timestamp=$(date +"%m/%d %H:%M:%S")
  echo "<code>$nick</code> <span>$msg</span> - <span class=small>$timestamp</span><br />" >> "$logfile"
  cat<<EOF
Content-Type: text/html

<meta http-equiv="Refresh" content="0; URL=$SCRIPT_NAME?nick=$nick" />
EOF
else
  cat<<EOF
Content-Type: text/html
Pragma: no-cache
Cache-Control: no-cache

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta http-equiv="Refresh" content="40; URL=$SCRIPT_NAME?nick=$nick" />
    <link rel="stylesheet" href="/css/bootstrap.min.css">
    <script src="/js/bootstrap.min.js"></script>
    <title>$title</title>
  </head>
  <body>
    <div class="container-fluid">
      <div class="row">
        <div class="col-md-2"></div>
          <div class="col-md-8">
            <h1>$title</h1>
            <div class="well">
EOF

  [ -f "$logfile" ] && tail -n $linenb "$logfile"

  cat<<EOF
            </div>
          </div>
          <div class="col-md-2"></div>
        </div>
        <div class="row">
          <div class="col-md-2"></div>
          <form class="form-inline" action="$SCRIPT_NAME" method="post" enctype="application/x-www-form-urlencoded">
            <div class="col-md-8">
              <p class="text-center">
                <input class="form-control" type="text" name="nick" size="5" placeholder="Name" value="$nick" />
                <input class="form-control" type="text" name="msg"  size="59" placeholder="Input message here." />
                <button type="submit" class="btn btn-default">
                  <span class="glyphicon glyphicon-comment"></span>
                </button>
              </p>
            </div>
          </form>
          <div class="col-md-2"></div>
        </div>
        <div class="row">
          <div class="col-md-2"></div>
          <div class="col-md-8">
            <hr />
            <p class="text-right">
              shellchat by <a href="http://dyama.org/" target="_blank">dyama.org</a>
            </p>
          </div>
          <div class="col-md-2"></div>
        </div>
      </div>
    </div>
  </body>
</html>
EOF
fi

exit 0

全体的にどべえーっと長ったらしい HTML タグがありますが、ほとんどがデザイン部です。実際に処理しているのは数行しかありません。 冒頭で bashcgi.sh を読み込んで、シェル変数に POST 送信の内容を詰め込んでいます。変数 nick と msg があれば、ログに追記した後、ユーザにはページをリフレッシュするよう META タグを返しています。また、変数がない場合はそのままログを表示しています。

セキュリティ的にも性能としても、何もやっていない分、いろいろと問題があるのですが、一応ちゃんと動いています。

shellchat

実際に動かすと、上記のように表示されます。

FastCGI+lighttpd で C プログラムを動かす

FastCGI(fcgi)とは、通常のCGIのようにリクエストが発生する度に起動、実行される仕組みではなく、一度起動したら起動しっぱなしでリクエストを処理するタイプのCGIです。常にメモリの上に乗っているので、メモリを食いますが、大量のリクエストをオーバーヘッドなしで処理できる利点があります。

FastCGIでバイナリプログラムを実行するケースのパフォーマンスはなかなか良いらしいので、我が家のlighttpdサーバにも導入してみることにしました。

久しぶりに lighttpd.conf を覗いてみると、モジュール読み込み部分に既に mod_fastcgi が登録されていました。(そう言えば、以前 trac を動かしてみた際に FastCGI を使ってたっけ…)

server.modules = (
"mod_access", "mod_alias",
"mod_accesslog", "mod_compress",
"mod_redirect",
"mod_expire", "mod_fastcgi"
(略)
)

こんな感じです。次に、FastCGIの設定を行います。

fastcgi.server = (
  "/fcgi" => (
    "fcgi" => (
      "socket" => "/var/run/lighttpd/fcgi.socket",
      "check-local" => "disable",
      "bin-path" => "/var/www/fcgi-bin/test.fcgi",
      "min-procs" => 1,
      "max-procs" => 2
    )
  )
)

bin-path には、これから準備するバイナリのパスを指定しています。
我が家では、上の fcgi の設定に加えて、trac の設定もしてあります。

次にバイナリの用意です。FastCGI用のライブラリを持ってきます。

[bash]
$ sudo aptitude install libfcgi-dev
[/bash]

bin-path で指定した位置にバイナリを作ります。

[bash]
$ cd /var/www
$ sudo mkdir fcgi-bin
$ cd fcgi-bin
$ sudo vim fcgi.c
[/bash]

fcgi.c の内容は次のとおりです。

[cpp]

include <fcgi_stdio.h>

int main(int c, char** argv)
{
int cnt = 0;
while (FCGI_Accept() >= 0)
printf("Content-type: text/html\n\ncount=%d\n", ++cnt);
return 0;
}
[/cpp]

fcgi_stdio.h をインクルードすると、printf() などの標準ライブラリ関数が FastCGI のものに置き換えられ、FCGI_Accept() などの関数が使えるようになります。

このコードをビルドします。-lfcgi オプションで FastCGI へのリンクもお忘れなく。

[bash]
$ sudo gcc -o test.fcgi test.c -lfcgi -Wall -O3
$ sudo chmod a+rx test.fcgi
[/bash]

念の為、全ての権限に rx を付与しています。
最後に設定を反映させる為、lighttpd を再起動します。

[bash]
$ sudo /etc/init.d/lighttpd restart
[/bash]

lighttpd.conf の fastcgi.server で設定したパスにアクセスしてみます。

[bash]
$ curl http://127.0.0.1/fcgi
count=1
[/bash]

何度もアクセスしてみて、count の値が増えつづければプロセスが常駐していることが分かります。

[bash]
$ curl http://127.0.0.1/fcgi
count=2
$ curl http://127.0.0.1/fcgi
count=3
$ curl http://127.0.0.1/fcgi
count=4
[/bash]