最終更新: 1998-10-02 (修正作業中)
このテキストはC MAGAZINE ?年?月号に掲載された原稿のオリジナルテキストを元にしてHTMLに変換したものです。 掲載文章と細部が異なっていると思われます。また、気付いた個所をいくつか修正してあります。
第1章 C言語によるCGI

------------------------------------------------------------------------

HTMLって何?

HTML(Hype Text Markup Language)は、WWWでデータをやりとりするのに使われている言語である。WWWの世界は既に流行というよりも実用に入っているようだが、HTMLの性質、すなわち簡単なものなら誰でも書け、複雑なことをすればかなり凝ったデザインも実現できる、という特徴が大きく影響していると言えよう。インターネット上のデータの動きは細かい所まで仕様として定められているが、WWWで自分のWeb Pageを公開しようというだけなら、下位のプロトコルについては何も考える必要はない。どのようにHTML形式のファイルを作るか、という程度の知識で済んでしまうのである。Web Page(*2)を公開するのに必要な手順は、ごく簡単に言えば次の通りである。

1. HTMLを使ったデータを作成する。 2. 作ったデータをサーバに置く。

サーバに置いたデータをインターネットのどこからでも見えるようにするには、サーバの設定が必要になるが、それはサーバの管理者がやることであって、いわゆる「ホームページ公開可」のようなサービスを行っているプロバイダに入会すれば、指定のディレクトリにデータを置くだけで、世界に向けて情報発信(*1)することができるという安易なシステムになっている。もっとも、細かいことを言えば、HTMLのデータを手元のパソコンで作ったが、どうやってサーバに置けばいいか、というあたりでいきなり悩む場合もあるのだが。最近はインターネットに関係していそうな雑誌が乱戦状態なので、とりあえず情報は十分だし、さらにインターネットの特徴としては、インターネットでどうすればよいかという情報が殆どインターネットで入手できてしまうというのがよい。

インターネットに置くデータは別に何でも構わないのだが、どのような形式のデータが置かれているか分らなくては参照する人の方が困る。そこで「こういう形式にしよう」という合意として成長してきた仕様がHTMLということになる。最近はHTMLもメーカーの思惑で勝手に拡張されたりして何がどうなっているのかよく分らない状況になっているのだが、とりあえずHTMLの仕様に従ってデータを書いておけば解釈できる人も多いという事実だけ認識しておけば、Web pageを作る側にとっては事実上不都合はないだろう。

というわけで、まとめると「インターネットに公開するWeb pageを書くための言語」という感じで理解しておけばいいと思う。

(*1) 情報を発信というよりは、とりあえず置くだけというのがイメージとしては分りやすい。全世界どこからでも見える仮想スペースを用意し、そこに置いてあるものを遠くから取りに来る、という感じである。

(*2) ホームページという表現が何を意味するかという問題がある。一般によく使われている意味としては、1.Webブラウザを起動した時に最初に表示されるページ、2.公開されているページの先頭となるページ、3.公開されているページの内容全て、などがある。本来の意味はどうだとか現実には何を意味するとか考えると話がややこしいが、例えば私のページの場合はhttp://www.st.rim.or.jp/~phinloda/ をホームページと呼び、それ以外のページはホームページではない、というように解釈するのが無難だろう。このように、最初にアクセスされることを目的としたページのことはWelcome pageと呼ばれることがある。

本文では、ホームページという表現は誤解をまねくおそれがあるので、Web pageという表現を使っている。これは上記の無難な解釈に従えば、ホームページおよびその付近のディレクトリに置いてある関連のページ全てをひっくるめたものである。

HTMLの書き方は?

HTMLの書き方をここで書いていたら予定のページが終わってしまうので、今回はHTMLはとりあえず書けるということで、…というのもちょっと無責任かもしれないで、よく使うタグだけ表にまとめておく。ちょっとだけ超基本的な話をしておくと、HTMLを使った文書書き方の基本はListのようになる。全体を<HTML>と</HTML>で囲んで、ここがHTMLで書かれていることを示す。その中は<HEAD>〜</HEAD>で囲まれた部分と、<BODY>〜</BODY>で囲まれた部分からなる。HTMLでは、このように「<」と「>」で囲まれた文字列のことをタグと呼んでいる。タグは文書の内容そのものではなく、文書の内容をどのように表示するとか、他のデータとの関連を付けるといった、付加価値情報を示すために用意されている。

- List -<HTML><HEAD><TITLE>ここにタイトルを書く</TITLE></HEAD><BODY>ここに本文を書く</BODY></HTML>- List end -

Listの「ここに本文を書く」という所に適当にテキストで文章を書くだけでも構わないのだ。ただ、最近は流石にWeb pageも凝ったものが多くなったので、テキストだけだと見栄えがちょっといまいちになると思うが、それでも結局情報の重要性は見栄えではなく中身である。また、現時点では家庭から使える回線速度はせいぜいISDN回線からマルチリンクppp接続を使って128kbps程度なものだから、あまり巨大なデータとか動画、音声などを置かれても、データを受け取るのに躊躇してしまうということもあるだろう。

HTMLについてさらに詳しく知りたいなら、「ホームページの作り方」のような感じのタイトルの本が今やいくらでも出ているので、それを見るのが手っとり早い。もし厳密な定義を知りたいというのであれば、RFC 1866がHTML 2.0の仕様になっているので、WWWで探してみるとよい。

典型的なHTMLタグ一覧
<HTML>〜</HTML>HTML文書
<HEAD>〜</HEAD>ヘッダ
<BODY>〜</BODY>本文
<H1>〜</H1>見出しエレメント(レベル1)
<H2>〜</H2>見出しエレメント(レベル2)
<H6>〜</H6>見出しエレメント(レベル6)
<B>〜</B>太字
<U>〜</U>アンダーライン
<I>〜</I>イタリック
<TT>〜</TT>タイプライタ風フォント
<S>〜</S>削除
<SUB>〜</SUB>添字
<SUP>〜</SUP>肩付き文字
<A HREF="URL">〜</A>アンカータグ(リンク)
<A NAME="NAME">〜</A>アンカータグ(フラグメント)
<IMG SRC="URL" ALT="text">グラフィックスの埋込み

特殊文字
文字表現方法
"&quot;
&&amp;
<&lt;
>&gt;

HTMLはどこに置く?

いわゆる「ユーザーホームページ」を公開できるプロバイダなら、どこに置いたらよいかというヒント情報を提供しているはずなので、プロバイダのホームページを調べてみること。例えばリムネットの場合、次のような手順を行う。

1. 会員に与えられたホームディレクトリにpublic_htmlというディレクトリを作成する。このディレクトリは少なくとも誰でも読めるような状態に設定されていなければならないので、chmod a+rw ~/public_html を実行しておく。

2. pubilc_html の下にHTMLのファイルを置く。特に、index.htmlという名前のファイルを置けば、これはデフォルトで参照するページになる。例えば、http://www.st.rim.or.jp/~phinloda/ と指定した時に、http://www.st.rim.or.jp/~phinloda/index.html が自動的に参照される。つまり、何かの機会にURLを紹介したい時に、index.htmlの部分は書かなくても済むので少しだけ楽だ。

3. public_htmlの下に、.htaccessという名前のファイルを置く。このファイルには、Listのような内容を書いておく。これはSSIとかCGIを実行させるのに必要な指定である。

---- List (.htaccess の中身) ----
AddType text/x-server-parsed-html .html
AddType application/x-httpd-cgi .cgi

HTMLとCGIの関係は?

WWWで公開するデータを記述する言語をHTMLだとすれば、CGIとの関係は何か。大雑把にいえば、CGIはHTMLの機能をさらに強化するためのスパイスのようなものである。HTMLはリンクという機能を使うことによって、選択したページにジャンプするという構造を作ることができる。しかし、これだけでは、ジャンプできる先のページは既に作成されたコンテンツに過ぎない。CGIを使えば、よりインタラクティブな、そしてダイナミックな処理を行うページを作成することが可能になるのである。

では、HTMLとCGIとは、具体的にはどういう関係があるのか。CGIとは、HTMLの中から呼び出されるプログラムとして位置付けられている。例えばHTMLの中に、イメージを埋め込む記述がある。<IMG SRC=..>というタグである。これと同じような書き方で、HTMLの中に、ここでCGIを実行しろという指定ができるのである。CGIを指定する方法は大きく分けると二つある。まず、直接CGIを指定する場合。後述のSSIを使ったり、アンカーを使ってCGIを呼び出すことができる。もう一つは、フォームを呼び出して最後にsubmitされた時に実行するメソッドとしてCGIを指定する書き方である。それぞれの例をfigに示す。

---- fig CGIの指定方法 ----
SSIを使う

  <!--#exec cgi="./cgi-bin/count.cgi"-->

アンカーの形式

  <A HREF="./cgi-bin/check.cgi">ここをクリックしてください</A>

formタグのactionとして指定する場合

  <form method=get action="./cgi-bin/test.cgi">
  ..
  </form>

こうやって呼び出されたCGIプログラムは何をするのか? 基本的に、プログラムを使ってできるようなことなら何でもできる。もちろん、システムに許可されていない、すなわちプログラムで実行が許可されていないことはできないが、HTMLだけではできなかったことが結構できるのである。

SSI

SSI(Server Side Includes) とは、HTMLの中に実行するコマンドを指定する機能である。通常は、サーバはHTMLファイルをそのままブラウザに送信するが、HTMLファイルの中にSSIがあると、サーバはそれをそのままブラウザに送信するのではなく、何等かの処理を行った結果に置き換えてから送信する。具体的にどのような処理をするかは、SSIの記述内容次第である。

ページを見ている側からは、そのコマンドを実行した結果があたかも最初からHTMLファイルに書かれているかのような情報を受け取ることになる。例えば、アクセスカウンタを簡単に実現したい時によく使われるSSIは、次のようなものである。

<!--#exec cmd="./count.sh"-->

これはHTMLとしてはコメントの形式なのだが、SSIをサポートしているサーバは、これをコメントではなくSSIとして解釈する。SSIは次のような形式を持っている。

<!--#コマンド パラメータ="引数"-->

例の場合は、コマンドがexec、パラメータがcmd、引数が./count.shということになる。execというのは指定した外部プログラムを実行して、現在のドキュメントにその出力を挿入するという命令なので、この場合はサーバでcount.shというプログラムが実行された結果を、HTMLのこの個所に埋め込むことになる。

アクセスカウンタというのは「あなたは2134人目のお客様です」のような文章の中に数字を埋め込むというものだから、SSIを使えば実現できることになる。

SSIは、WWWサーバが使用許可の設定になっていなければ使うことはできない。プロバイダによっては使用できない場合もあるので、あらかじめ確認しておこう。

SSIの機能を使ってCGIプログラムを呼び出す場合、execコマンドのパラメータとしてcmdではなくcgiを指定する。次のような行をHTMLに埋め込んでおく。

<!--#exec cgi="./cgi-bin/counter_cgitest.cgi"-->

------------------------------------------------------------------------

CGIって何?

CGIは、Common Gateway Interfaceの頭文字をつなげた略語である。HTMLだけでは実現できないインタラクティブな処理を実装するために使う機能である。

ブラウザがWWWサーバから送られてきたHTML形式のデータを受け取ると、それを解釈しながら手元のpcなどの画面上に表示する。この中にリンク情報などがあれば、ブラウザ上の該当個所をクリックした時に、ブラウザはWWWサーバに対して、次に送って欲しいデータのURLを通知する。WWWサーバはこの要求に従い、指定されたURLに応じたデータをブラウザに送る。この繰り返しがネットサーフィンの正体である。

ところで、WWWサーバ上に置かれているデータをそのまま送信するだけなら簡単なのだが、それだけでは少し物足りない、もっとインタラクティブなやりとりをしたいとか、毎回同じテキストを送るのではなく、状況に応じて異なった結果を返したいという欲求が出てくる。これを実現するための機構の一つがCGIなのである。

WWWサーバはCGIを次のような手順で処理する。

1. CGIを実行するリクエストを受け取る。

 これは、ブラウザからのformの出力であったり、SSIの中の指定であったり、実際の要求メカニズムはいくつかあるが、要するにプログラムを実行しろという要求を誰かが出すのである。これをWWWサーバーは受け取る。

2. CGIプログラムを実行する。

 WWWサーバーは、指定されたプログラムを実行する。従って、CGIの正体はプログラムそのものということになる。HTMLのデータをそのまま出力するのならプログラムで処理する必要はない。CGIの多くは、既存のデータを組み合わせたり、その場で状況に応じてHTMLのデータを生成する。

 CGIはその場で動く単なるプログラムである。だから、プログラムに出来ることであれば何でもできる。アクセスカウンタの集計をしたり、その記録をファイルに保持したり、全く関係ないゲームのプログラムを呼び出したり、とにかく何でも理屈の上では実行可能である。このことがCGIの欠点にもなっている。すなわち、システム破壊を目的とする場合に、CGIは絶好のチャンスになる危険を持っているのである。WWWサーバによってはCGIを実行を許可していないことがあるが、どんなプログラムが実行されるか分らないというのは極めてリスキーなギャンブルになってしまうからである。

3. CGIプログラムの出力をHTMLとして解釈し、ブラウザに送信する。

 CGIは基本的に標準出力へ結果を掃き出す。この情報をWWWサーバーが受け取って、適切な処理の後、ブラウザに何らかのリアクションとして送り出すのである。基本的にはCGIの出力がそのままブラウザに出て行くのだか、特別な指定をすれば、他のページを出力することもできる。

CGIを使えば何ができる?

CGIを利用する典型的な例を3つ紹介する。

1. アクセスカウンタ

「あなたは24164人目の訪問者です」のような表示を見た経験のある人は多いだろう。HTMLに書いた内容をそのままブラウザに表示するだけでは、このようなカウンタは実現できない。カウンタを実現するためには、サーバがデータを要求される度にカウントアップした情報を戻してやるような仕組みが必要になる(*1)。このように、呼び出された時の状態によって変化するドキュメントのことをダイナミックドキュメントと呼んでいる。アクセスカウンタというのは、ダイナミックドキュメントの一種だと考えることができる。このようなダイナミックドキュメントを生成するためには、あらかじめHTMLのデータを生成するようなプログラムを用意しておき、サーバが呼び出された時にそれを実行して適切なHTMLデータを含んだ情報をブラウザに送信するようにすればよいのである。CGIはこのような用途に適している。

(*1) アクセスカウンタの実現方法としては、イメージを埋め込む方法もある。この場合は該当ページを置くWWWサーバーがCGIをサポートしていなくても、全く別のサーバで生成したイメージを埋め込むことによってカウンタを実現することができる。

2. クリッカブルマップ

画面上のどこかをクリックして、その位置によって対応するページにジャンプする。このようなページも見たことがあるだろう。これもCGIを使って実現できる機能である。ブラウザは、表示した画面の特定の位置がクリックされたら、それをCGIの形式として処理するようにサーバに要求する。サーバは位置情報を解析するCGIを実行して、それに応じた処理を行うのである。

但し、Netscape Navigatorなどのブラウザが機能を拡張したため、特にサーバー側で処理しなくてもクリッカブルマップを実現することができるようになった。この機能を使えばサーバの負荷は軽減できるので、ブラウザ側で処理するような書き方にもメリットがある。

3. フォーム

WWWで行われているアンケートや、感想を書くコーナー。掲示板、訪問者リストなど。ブラウザ上で選択したりテキストを入力して、その結果をサーバーに送る機能がフォームである。フォームを用いて生成されたデータはサーバーに送られるが、問題はその後の処理である。この処理を行うのもCGIの役割である。

CGIは、フォームを集計してデータをファイルに掃き出すように設計してもいいし、得られた情報を特定のアドレスへe-mailとして送信するように設計することもできる。とにかくプログラミング可能なことはたいていCGIを使って実現できるのである。HTMLはサーバから情報をブラウザに送ることはできても、ブラウザからの情報をダイナミックに受け取るのが苦手である。この分野の処理をCGIが手助けするという仕組みである。

(*1) HTML形式であるとは限らない。ftp、newsなどの形式のURLが指定されていると、それに応じた形式の情報が送られてくる。

CGIはどうやって作るのか?

CGIプログラムは、どのようなものを使って実現しても構わない。サーバーで実行できるようなプログラムなら何でも構わないのである。よく使われるのは、シェルスクリプト、Perlのスクリプト、CやC++のプログラムなどである。特に、Perlのスクリプトで書かれたCGIプログラムが多いと言われている。これは、Perlの持っている強力な文字列処理機能と、かなりシステムに近いレベルの細かいことまで操作できる多機能性がウケているためだと思われる。実際、CGIを呼び出す時に与えられる情報は、文字列としてやって来るため、それを処理するのはPerlが得意ということになる。

ではなぜC言語でCGIなのか? C言語は確かにPerlに比べると面倒だし、特に文字列のパターンマッチの処理は貧弱である。同じような処理をするのにも、Perlに比べると原則として長いプログラムを書かなければならないことの方が多い。しかし、C言語で書いたCGIプログラムも廃れていない。やはり、それだけの価値があるからなのである。最も大きな利点は、C言語で書いたプログラムを実行するのに必要な資源は、Perlで同じことをするよりも圧倒的に少なくて済むことが多いし、処理時間も短くて済むということである。要するに軽いのである。だから、サーバの負荷も軽くて済むし、応答も早いはずである。頻繁に呼び出されるようなCGIだと、このメリットは大きい。

もっとも、C言語でCGIを実装しようという人にとって最も大きなメリットは、C言語に慣れているということかもしれない。Perlはそれほど難しい言語ではないし、C言語でプログラムが書ける人ならごく簡単な学習だけでマスターできる。しかし「もうC言語が使える」という状態の人が、それを使って何かしようと思うのは自然な発想だろう。ある状態にいる人が別の状態に移るには、かなりのエネルギーを必要とするものである。

CGIはどうやって情報を受け取るのか?

既に書いたように、CGIの実態ははWWWサーバ上で動作するプログラムである。さて、ブラウザから何か操作して、その結果をCGIで処理する場合を考えてみる。CGIはブラウザから何等かの情報を受け取る必要がある。これは一体どうやってブラウザからCGIへ伝えられるのか。まず、ブラウザは操作した結果をWWWサーバに知らせる。WWWサーバは、ブラウザから受け取った情報を、環境変数として、あるいは標準入力から入力された情報として、CGIに渡すのである。CGIプログラムは、これらの情報を受け取って処理することにより、ブラウザから得られた情報を知ることができる。

CGIプログラムは、まず環境変数"REQUEST_METHOD"を参照する。この値は"GET"あるいは"POST"のいずれかのはずである。値に応じて、次のように処理をする。

1. GETの場合

 情報は環境変数QUERY_STRINGの値として引き渡されている。従って、この値を処理すればよい。

2. POSTの場合

 情報は標準入力から引き渡される。受け取るデータの長さは、環境変数"CONTENT_LENGTH"に格納されている。従って、この長さだけ標準入力から読み込んで、その結果得られた文字列を処理すればよい。

 POSTが指定された場合に、CONTENT_LENGTHバイトを標準入力から読み込んでも、その後にEOFが来る保証がない。従って、Listのような処理で読み込もうとすると、いつまでたっても処理が終わらないかもしれない。

---- List ----
  /* これは危険 */

  while ((c = getchar()) != EOF) {
    /* 読み込んだ情報の処理 .. */
  }

こうやって得られた文字列は、&で区切られたkey=value形式のデータになっている。つまり、個々の情報を取り出すには、次のような手順を行う。

1. &があれば、それを区切り文字として、バラバラにする。

2. その結果はkey=value形式であり、「=」をはさんで左右に分れているから、keyとvalueを取り出す。

これでよいのだが、少し問題があって、漢字コードとか特殊文字はどうなるのか、ということに注意が必要である。次の二つのルールによって情報は単純にエンコードされている。

1. 空白があれば「+」に置き換えられている。従って、「+」という文字があったら、それを空白「 」に置き換えてやればよい。

2. 特殊文字は、%に続く2文字を16進数とみなして解釈できるようにエンコードされている。従って、%が現れたら、続く2文字を含めた合計3文字を、その2文字に対応した1バイトの値に置き換えてやればよい。

CGI環境変数

CGIは、呼び出された時点で、CGI環境変数と呼ばれている環境変数を受け取ることができる。

CGI環境変数
SERVER_SOFTWAREサーバソフトウェアの名前とバージョン
SERVER_NAMEサーバのホスト名、またはIPアドレス
GATEWAY_INTERFACECGIのリビジョン
SERVER_PROTOCOLプロトコルの名前とリビジョン
SERVER_PORTリクエストが送られて来たポートの番号
REQUEST_METHODリクエストが作られたメソッド。GET、POSTなど。
PATH_INFOエキストラパス情報
PATH_TRANSLATEDPATH_INFOを変換した情報
SCRIPT_NAME実行されるスクリプトの仮想パス
QUERY_STRINGクエリー情報
REMOTE_HOSTリモートホスト名
REMOTE_ADDRリモートホストのIPアドレス
AUTH_TYPE認証メソッド
REMOTE_USERユーザの認証名
REMOTE_IDENTリクエストを出しているユーザ
CONTENT_TYPEデータのコンテントタイプ
CONTENT_LENGTHデータの長さ
HTTP_ACCEPTクライアントが受付けるMIMEタイプ
HTTP_USER_AGENTクライアントがリクエストを発行するブラウザ

uncgi

GETで得られる環境変数をQUARY_STRINGを処理するのは結構大変である。&で区切られた内容を分離し、=の左右に分け、クオートされたコードを元に戻す、とこう書いただけなら随分簡単に見えるが、Cプログラムで書こうとすると結構大騒ぎだ。実は、この処理を簡単にするライブラリというのが世の中には既に存在する。その一つがuncgiというプログラムである。uncgiは、QUARY_STRINGの内容を解析して、それぞれのパラメータに対応した新たな環境変数を作り出す。例えば、QUARY_STRINGが"param1=ok&param2=default"という内容だったとしよう。これをuncgiに渡した後は、WWW_param1という環境変数がokという値になり、WWW_param2という環境変数がdefaultという値になる。このような状態になっていれば、その後、それぞれのプログラムが状態判断するのに便利である。特に、cshとかshが判断するのには都合がいい。

CGIはどうやって情報を戻すのか?

CGIは標準出力に情報を書き出す。WWWサーバは、これを元にしてブラウザに必要な情報を送信する。

CGIの出力は、HTTPヘッダと、それに続くデータによって構成されている。HTTPヘッダの後には必ず空白行を置く必要がある。HTTPヘッダに含めることができる情報はいくつかあるが、典型的なものを表にする。

---- fig (典型的なHTTPヘッダ) ----
Content-type     出力のMIMEタイプ
Content-length   出力の長さ
Location         サーバリダイレクション

ダイナミックドキュメントを生成する場合によく使われるのはContent-typeである。HTMLのデータを出力するのであれば、これをtext/htmlと指定する。gif形式の画像データを出力するのであれば、image/gifと指定して、サイズをContent-lengthに指定する。

CGIが与えられたパラメータの処理だけを行えばよい場合は、サーバリダイレクションと呼ばれる機能を使うのが便利である。サーバは、CGIの出力の代わりにLocationで指定したURLにあるデータをブラウザに送信してくれる。HTTPヘッダは必ず空白行で終わらなければならない。サーバリダイレクションを用いる時には、Locationを指定した後に空白行を送ることになる。

---- fig (CGIの出力) ----
   HTTPヘッダ  |  Content-type: text/html
   空白行      |  
   HTML本文    |  <HTML><HEAD><TITLE>cgi test page</TITLE></HEAD>
               |  <BODY>
               |    (略)
               |  </BODY>

作ったCGIプログラムをどこに置けばよいのか?

Cで書いたプログラムをコンパイルした場合、実行ファイルの名前に特に制限はない。UNIX環境なら、何も指定しなければa.outのような名前のファイルが出来る。CGIプログラムの場合は、標準として.cgiという名前にすることが多い。だから、a.outではなくてa.cgiのように名前を変えておく。この実行プログラムをどこに置けばよいか。結論は、サーバの設定次第である。指定した場所のプログラムだけをcgiとみなすように設定されていることもある。このように場所を指定する理由は、主にセキュリティの問題を回避するためである。設定次第では、どこにcgiプログラムを配置しても構わないが、Web pageの作者としては、あまりあちこちにcgiプログラムが分散していると、かえってややこしいという考え方もある。多くのページで、cgi-binというディレクトリを用意して、そこにcgiプログラムを集めるような構成が取られている。特に理由がなければ、これを真似するというのも無難である。

CGIプログラムの動作する環境

CGIプログラムは、基本的に、サーバ上のnobodyというユーザが実行することになる。従って、CGIプログラムは、誰でも読めて実行できる所に配置しなければならない。CGIプログラムがアクセスするファイルがある場合、それは誰でも読めるようにしておかなければならない。また、何かファイルを作成するのであれば、そのディレクトリは誰でも書き込めなければならない。

特に、誰でも書き込めなければならない状態にするというのは、かなり危険なことであることを頭に入れておいて欲しい。インターネットでアクセスできるファイルが誰でも書き込めるということは、まさに誰でも書き込めるという状態なのである。インターネットのような大規模コミュニティでは、変人存在法則(その中に必ず変な人がいる)が成り立つ。いきなり全部ファイルを消すのが趣味の人とか、他人のディレクトリにポルノデータを書き込むのが趣味の人もいる。そのような人達を歓迎するか、それとも遠慮してもらうか、それは個人の自由であるが、とにかくそのような状況を想定してみる価値はある。

少し視点を変えて、環境そのものについて考えておこう。CGIプログラムを実行できるようなプロバイダは、シェル環境でログインできるようなサービスを提供している場合がある。この場合、UNIXのマシンにログインして、その環境でコンパイルしたり、Perlスクリプトを実行したりできる。ところが、CGIスクリプトが呼ばれた時には、これとは全く別の環境からプログラムが実行されることになる。特に最近のUNIX環境では、実行時にリンクするという形式のオブジェクトがある。これは資源節約の点では有利なのだが、リンクするモジュールがどこにあるのかを知らないと、実行できないという欠点も生じる。つまり、とりあえずシェル環境にログインして直接実行してみたら問題なかったとしても、同じものをCGIとして実行できない、ということがあるのだ。これは、実行プログラムを作る時に、いわゆるstatic linkを指定することによって解決する。gccの場合、コンパイル(リンク)オプションとして-staticを指定しておけばよい。

RIMNETの場合、-staticが指定されていないとエラーになるようである。最初、このエラーが出た時に、あまりに謎のメッセージが出たので一体何がいけないのか分らなかったのだが、結局、-staticオプションを付けることにより解決した。ダイナミックリンクライブラリが指定されているパスの中には見つからなかったため、cgiが正常に実行できなかったというエラー表示が出たのだろう。このようなエラーが出た場合は、サーバの管理機能を使うことができれば、エラーメッセージを確認してデバッグすることも可能だが、プロバイダに入会している会員という立場では、そこまで教えてくれるかどうか、あまり期待できない。

参考になるページ

WWWの使い方はWWWからいくらでも入手できる。しかし、うっかりAltaVistaやGooなんかで「CGI」で検索したりすると、数万ヒットしてしまってどこから何を見てよいやらさっぱり分らないという結果になる。Yahoo Japanなどで、コンピュータのカテゴリの中にCGIという分類がある。そこだけでも数十以上のリンクがすぐに見つかる。さらにリンクのあるページから順ぐりにたどっていけばよい。というわけで、たくさん紹介してもしょうがないから、ちょっと見た範囲で特に参考になると思ったページを10だけ紹介させていただく。

0からはじめるCGIプログラミング
かなり丁寧な内容で、一から始める人にもわかりやすい。

CGI/SSIコレクションルーム
CGIやSSIを集めたページ。コレクションは充実している。まずは既存のCGIを参考にしようという人向け。

gccでCGIを作ろう
gccを使ってCGIするというページ。C言語をあえて選んでいる所がいいが、この原稿を書いている時点では工事中の所が少し多い。

CGI・Perl入門

Total introduction to programing CGI in C
http://www.hongo.ecc.u-tokyo.ac.jp/~s61578/CGIINTRO/cgiintro.html
(Not Found) UNIX C言語によるCGIの作成入門。簡潔にうまくまとまっているが、少しレベルが高いかもしれない。かなり参考になると思う。

MAKING INTERACTIVE HOMEPAGE
CGIを使ったページ作成方法。最近更新されていないが、かなり完成度の高いページ。Perlの分る人にはおすすめ。

Making INTERACTIVE PAGE(How to make CGI and SSI)
ここも有名なページ。完成度は高い。CGIの落とし穴的話題にも触れている。

The Common Gateway Interface
本家。英文のページ。test-cgiのページでサンプルをクリックすると、CGI変数がどうなっているか表示したりできる。

CGI Scripting in C
http://www.he.net/~searsbe/ScriptingInC.html
(Not Found) 英文のページ。C言語でCGIするというアプローチ。

The cgi-lib.pl Home Page
Perlを使ったcgiライブラリを公開している。

Un-CGI
Un-CGIを公開しているページ。

------------------------------------------------------------------------

CGIの使い方

自分のWeb pageにCGIを埋め込むのは、ある意味では簡単だし、別の意味では難しい。だから、簡単に使えるかと尋ねられても、何とも言えないというのが回答になってしまう。ただし、これからCGIに挑戦しようという人のために、まず頭に入れておいて欲しいことを二つ述べておく。

1. CGIは、それを利用できるように設定されたサーバでのみ使うことができる。

 つまり、CGIを許可されていないサーバ上だと、どんなに頑張ってもCGIは使えない。Web pageをプロバイダに置きたいのなら、CGIが許可されたプロバイダに置かないと意味がない。自分でWWWサーバを用意するというパワーユーザの方もいるかもしれないが、その場合はCGIが使えるようにサーバを設定する必要があるが、具体的な方法は今回の特集のレベルをはるかに越えてしまうので、今回は省略させていただく。

2. 正しくないCGIは危険である。

 CGIは、サーバの上で動作するプログラムである。プログラムがそれを書いた人の意図通りに動いているうちはよいが、意表を突いた使われ方をしたり、あるいはちょっとしたバグがあったりすると、システム全体に影響を与えかねない。

 特にしばしば指摘されるのが、セキュリティの問題である。CGIを使えば、ブラウザで入力したコマンドをサーバで実行するような機能を作ることもできる。ホームディレクトリ上でこのような機能を実行することができれば、確かに便利かもしれない。サーバがUNIXマシンなら、duコマンドを実行させたり、ls -lRコマンドを実行させて、現状をリモート環境から確認することができそうである。しかし、同様に、rm -rfや、mailのようなコマンドも実行できてしまうと、誰かがそのようなことを思い付いた途端に極めて危険な状態になってしまうのである。

 また、そのような意図がなくても、Cで書かれたプログラムは、さまざまなトラブルを発生させる原因になるかもしれない。ファイルを無制限に作ったり、子プロセスを次々と生成させるようなバグがあったら、システムをダウンさせたり、ダウンにまで至らなくても非常にまずい結果になる危険がある。従って、CGIを使うつもりなら、少なくともシステム上でどのようなことが許されていて、何をしても構わないのかということを正確に把握していなければならない。

-----------------------------------------------------------------------

CGIを呼び出すHTML

Listは、今回紹介するサンプルを呼び出すページである。これは私のページから「CGIテストのページ」へのリンクを作っておくので、WWWにアクセスできる人は、実際にそこを見ていただければ動いている様子が分る。

<!--#exec cgi="./cgi-bin/counter_cgitest.cgi"-->

SSIを使って、カウンターを起動している。このページをアクセスする毎に カウントが増えるはずなのだが…。

<FORM METHOD=GET ACTION="./cgi-bin/y_counter_1.cgi"><INPUT TYPE="submit" VALUE="QUERY"></FORM>

 この個所は、submitボタンを押した時にカウントアップするカウンタである。 このカウンタプログラムは、同じCGIプログラムを使って複数のカウンタ機 能を実現しているので、それがうまく稼働しているかどうかのテストになっ ている。

これはFORMからactionの指定によってcgiプログラムを呼び出す例になって いる。

フォームの例

フォームに使われる情報をとりあえず並べたというだけのサンプル。

残りは特に説明する必要もないはず。アンカータグを使ってCGIを呼び出してHTMLデータを受け取っている。

---- List (CGIを使ったページ) ----
<html>
<head><title>cgi test page</title></head>
<body>
<H1>CGIテストページ</H1>

ま、とりあえず…version up!
<p>

<!--#exec cgi="./cgi-bin/counter_cgitest.cgi"-->
<p>

カウンタとか。<br>
<FORM METHOD=GET ACTION="./cgi-bin/y_counter_1.cgi">
<INPUT TYPE="submit" VALUE="QUERY">
</FORM>
<p>

もう一つのカウンタ。上と同じcgiを使っている。<br>
<FORM METHOD=GET ACTION="./cgi-bin/y_counter_2.cgi">
<INPUT TYPE="submit" VALUE="QUERY">
</FORM>
<p>

<a href="formlist.htm">フォームの例</a>
<p>

カレンダー。
<FORM METHOD=GET ACTION="./cgi-bin/calender.cgi">
YEAR:<INPUT TYPE="text" NAME="YEAR" SIZE=4><br>
MONTH:<INPUT TYPE="text" NAME="MONTH" SIZE=2><br>
<INPUT TYPE="submit" VALUE="CALENDER">
</FORM>
<p>

<a href="./cgi-bin/oneline1.cgi">1行掲示板</a>
<p>

<a href="./cgi-bin/random.cgi">ランダムセレクタ</a>
<p>

<a href="./cgi-bin/randcg.cgi">ランダムCG LINK</a>
<p>

<a href="./cgi-bin/newhtml.cgi">最新ページ</a>
<p>

</body>
</html>

-----------------------------------------------------------------------

アクセスカウンタ

これがまず定番というか、CGIを使ってまず誰でも作ろうと思うらしい。カウントして何が面白いのかという話もあるが、なぜか付けたくなるのがカウンタである。

ただカウントするのでは面白くないので、私のページのアクセスカウンタは、特別な番号になった時には特別のメッセージを出すようにしてある。この処理は、カウントアップした時にある値になっていれば特別な文字列を表示することで簡単に実現できる。

アクセスカウンタの一番のポイントは、排他制御をどうするかということである。サーバには多数の人がアクセスするだろうから、アクセスカウンタを殆ど同時に二人の人がアクセスすることがあるかもしれない。すると、どのようなことが起きるだろうか。カウンタは、内部処理としてはカウンタ保存用のファイルに数値を書くというような方法で実現することが多い。従って、そのファイルをオープンして数値を読み、それに1を加えた数値を新たに書き込む、というような手順がまず考えられる。この手順を同時に二人が行うと正常にカウントアップされないことが容易に分るだろう。安易にカウンタを作った場合、カウンタがリセットしてしまったというトラブルが発生することがあるが、排他制御を怠ったのが原因ということもある。実は私もそうなったことがあるのだが、趣味のページのカウンタだからどうでもいいや、という程度に軽く考えても構わないのだらデタラメでも別に問題ないという考え方があるわけだ。

ちゃんと管理しようとするなら、アクセスカウンタは、カウンタ保存用のファイルを同時に二人がオープンしないようなロック機構を用いることになる。ロックというのは、簡単に言えば、特別な名前でファイルを作成してしまうという方法を使って排他制御するわけである。もし何もファイルがなければロックファイルを作成することができるが、既にロックファイルがある場合に、後から同じ名前のファイルを作成しようとしても、既にファイルがあるわけだから、エラーが発生する。こうなった場合には、少し待ってからもう一度実行してもらうのである。このあたりの機構は、UNIXのシステムコールがサポートしているから、それを使えばかなり確実にロックをかけることができる。

---- List (ロック) ----
/*====================================================================*/
static int lock_fd;

void wait_lock_counter(void)
{
    struct flock lock;

    lock.l_type = F_WRLCK;
    lock.l_start = 0;
    lock.l_whence = SEEK_SET;
    lock.l_len = 0;

    fcntl(lock_fd, F_SETLKW, &lock);
    /* F_SETLKWの場合、既にロックされている場合はプロセスがブロックされる。
     * ロックが解除されるまで、ここで待つことになる。
     */
}

void unlock_counter(void)
{
    struct flock lock;

    lock.l_type = F_UNLCK;
    lock.l_start = 0;
    lock.l_whence = SEEK_SET;
    lock.l_len = 0;

    fcntl(lock_fd, F_SETLK, &lock);
    close(lock_fd);
}

/* ALARM_INTERVAL 秒後に処理が終了しない場合に実行される。
 * もしかするとunlock_counterは二度呼ばれるかもしれないが、無視している。
 */
void sig_alarm(int signo)
{
    unlock_counter();
    cgi_error("timeout");
    /* ↑エラー時にブラウザに発生を知らせるためのHTMLを出力する関数 */

    /* timeoutの処理 */
    exit(0);
}

void lock_counter(void)
{
    signal(SIGALRM, sig_alarm);
    alarm(ALARM_INTERVAL);
    lock_fd = open(LOCK_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0600);
    if (lock_fd < 0) {
        cgi_error("can't create lockfile");
        exit(0);
    }

    /* 他からロックされている場合は待つ */
    wait_lock_counter();
}

int main(int argc, char *argv[])
{
    lock_counter();
    cgi(argv[0]); /* cgiの処理 */
    unlock_counter();

    return 0;
}

ところで、実際にアクセスカウンタを作る場合、最初のページ(welcome page)だけでなく、コーナー毎にどれだけ参照されたか知りたくなる。実は、これはサーバの保守機能を使えば集計されていて、わざわざCGIを使わなくても人気調査ができるはずなのだが、とりあえず複数のカウンタを使いたい場合にどうするかということを考えてみよう。それぞれのカウンタに対応させたCGIプログラムを用意するのも手である。しかし、C言語のプログラムをコンパイルするといろいろライブラリがリンクされてしまった結構大きなサイズになる。これは普通の感覚だと何でもないサイズなのだが、プロバイダによっては個人で使えるファイル容量の合計が1MBとか3MBというサイズに制限されているから、あまり大きな実行ファイルをたくさん配置したら、肝心の情報が掲示できなくなってしまった、という笑い話になりかねない。

そこで、実際に私が使っているカウンタは、複数のCGIを一つのプログラムで処理するように工夫してある。どうやってカウントを分けるかというと、cgi-bin/ディレクトリの中にシンボリックリンクを作っておくのである。例えば、カウンタ本体がcounter.cgiという実行プログラムだとすれば、

ln -s counter.cgi counter_1.cgi

のように実行して、counter_1.cgiからcounter.cgiへのリンクを作成する。その結果、counter_1.cgiを実行した時にもcounter.cgiのプログラムが起動される。一つのプログラムを複数の名前で起動できることになる。プログラムの中では、Listのような処理を行っている。create_command_name関数は、argv[0]を引数にして呼ばれる。コマンドはフルパスで呼ばれるかもしれないから、「/」をサーチして、ディレクトリ名を削った一番下の本体部分だけを取り出す。

Listの中ではmessage_typeという変数に-1、0、1の値を格納している。これは、後の処理でカウントした結果をどのように表示するかを使い分けるための変数である。-1の時には何も表示しないで、単にカウントアップだけ行う。0の時は、カウントした結果を表示する。1の時には、ジャストナンバーになった時に異様な表示を行う。

---- List (コマンド名からカウンタを使い分ける) ----
/* コマンド名を生成する
 * カウンタファイルの内容は、コマンド名により識別される。
 */
static int create_command_name(char *s)
{
    char *cmd = s;

    while (*s != '\0') {
        if (*s++ == '/') {
            if (*s)
                cmd = s;
        }
    }

    command_name = malloc(strlen(cmd) + 1 + 1);
    if (command_name == NULL) {
        return -1;
    }
    strcpy(command_name, cmd);
    strcat(command_name, ":");

    if (*command_name == 'x' && command_name[1] == '_')
        message_type = -1;
    else if (*command_name == 'n' && command_name[1] == '_')
        message_type = 0;
    else
        message_type = 1;

    return 0;
}

-----------------------------------------------------------------------

フォームの処理

フォームの処理なんか、どのCGIの本を見ても書いてあるので今更書くのも気がひけるのだが、一応書いておかないと後でレファレンスに使えないので、とりあえずありきたりの処理を一覧として紹介しておく。

<FORM ACTION="cgiプログラム" METHOD="メソッド">〜</FORM> フォームの開始、終了。この範囲の内容がフォームとなる。

<INPUT TYPE="text" NAME="name" VALUE="value" SIZE="size"> テキストフィールド

<INPUT TYPE="password" NAME="value" VALUE="value" SIZE="size"> パスワードフィールド。テキストフィールドと違って、入力した文字はエコー バックされない。

<INPUT TYPE="hidden" NAME="name" VALUE="value"> 隠蔽フィールド。これはブラウザの画面には表示されない。しかし、ブラウ ザにこの情報自体は送信されている。従って、ブラウザの機能に「ソースの 表示」があれば、それを見て隠蔽フィールドの内容を知ることができる。

<INPUT TYPE="checkbox" NAME="name" VALUE="value"> チェックボックス

<INPUT TYPE="radio" NAME="name" VALUE="value"> ラジオボタン

<SELECT NAME="name" SIZE=1><OPTION SELECTED> 1番目の項目<OPTION> 2番目の項目 …</SELECT> 選択メニュー。

<SELECT NAME="name" SIZE=n MULTIPLE> スクロールリスト

<TEXTAREA ROWS=yy COLS=xx NAME="name"> …</TEXTAREA> マルチラインテキストフィールド

<INPUT TYPE="submit" VALUE="Message"> Submitボタン。これを押した所でフォームに入力されたパラメータを付加し た状態でCGIが呼び出される。

<INPUT TYPE="submit" NAME="name" VALUE="value"><INPUT TYPE="reset" VALUE="Message"> Resetボタン。これを押すと、フォームの全ての項目は初期状態にリセット される。

-----------------------------------------------------------------------

カレンダー

ダイナミックドキュメントの一つの応用例としては、アクセスした時に現在は何日、何時です、というような表示を行うページが、探せば結構ある。もう少し凝ったものとして、現在時刻に応じてメッセージを変えるようなダイナミックページも最近は増えてきた。夜にアクセスすると、ご自宅からアクセスですか、とか、残業ですか、というようなメッセージを出すわけである。このようなメッセージは利用者が日本にいると決め打ちしているようなものだが、どうせ日本語表示のページを見る人の殆どが日本からアクセスしているだろうから、とりあえず愛嬌で済むだろう。昼と夜で背景を変えてみるようなページもある。ページ構成そのものを変えてしまうということも可能で、昼は会社からアクセスしても安心な画面、夜はきわどい画面、というような使い分けもできる。

ここでは、そこまで凝らないが、やや凝った例として、アクセスした月のカレンダーを表示してみる。

アイデアは極めて簡単である。指定月のカレンダーをHTML形式のデータとして生成すればよいだけの話である。このプログラムで生成したデータは、図のような形式を使ったTABLEの形式になっている。

---- fig ----
<TABLE BORDER>
<CAPTION>1997年3月</CAPTION>
<TR>
 <TH><font color="#FF0000">日</font></TH>
 <TH>月</TH><TH>火</TH><TH>水</TH><TH>木</TH><TH>金</TH><TH>土</TH>
</TR>
  〜 (略) 〜
<TR>
<TD><font color = "#FF0000">23</font></TD><TD><B>24</B></TD>
<TD>25</TD><TD>26</TD><TD>27</TD><TD>28</TD><TD>29</TD>
</TR>
  〜 (略) 〜
</TABLE>

fontタグのcolor指定が#FF0000となっているが、これは日曜日の所だけ赤色で表示させようとしているのである。当日は<B>と</B>で囲んで表示しているため、太字になる。

このカレンダーを実行するHTMLの記述の例は、Listのような感じになっている。YEARとMONTHをテキストフィールドを使って入力させているが、これは数値をリストにしておいて選択させるという方法もあるので、検討の余地はあると思う。

---- List ----
<FORM METHOD=GET ACTION="./cgi-bin/calender.cgi">
YEAR:<INPUT TYPE="text" NAME="YEAR" SIZE=4><br>
MONTH:<INPUT TYPE="text" NAME="MONTH" SIZE=2><br>
<INPUT TYPE="submit" VALUE="CALENDER">
</FORM>

Listの処理は、フォームの中で年と月を指定してからsubmitのボタンが押されることを想定している。CGIプログラムはホームページのディレクトリから相対指定で./cgi-din/に入っているcalender.cgiを指定している。FORMはGETなので、calender.cgiは環境変数QUERY_STRINGの値を処理することになる。このように、テキストを入力するフォームであっても、SIZEが4とか2という小さい値で指定されている場合は、環境変数を使ってデータを引き渡しても問題ない。

プログラムの一部から、環境変数のチェック部分だけ紹介しておく。check_environment関数は、環境変数REQUEST_METHODの内容によって処理を切り分けている。この内容がGETの場合には、環境変数QUERY_STRINGの内容をgetenv関数で取り出している。POSTの場合には、環境変数CONTENT_LENGTHで与えられた長さだけ、標準入力から取り込む。これらの結果は、mallocで確保された領域の中に文字列として格納する。いずれの場合も、成功すれば確保された領域の先頭アドレスを戻り値として呼び出し側に返る。

こうやって得た内容は、key1=value1&key2=value2&.. という形式になっているから、key、valueを調べて、必要な情報を取り出さなければならない。この処理は関数param_to_intで行っている。カレンダープログラムは数値だけを情報として読み込むことを想定しているから、少し処理をさぼって、目的のキーを発見したら「=」の右側の値を数値とみなして与えられたポインタの先に書き込むという処理だけになっている。本来は、ここで+を空白に置き換えたり、特殊文字があったらデコードしたりしなければならない所だ。

---- List (calender.c の一部) ----
/*--------------------------------------------------------------------*/
/* 環境変数をチェックして、CGIの引数を得る。
 * 結果はmallocした領域にコピーし、その先頭アドレスを返す。
 * 失敗した場合はNULLを返す。
 */
static char *check_environment(void)
{
    char *s;
    char *param;

    s = getenv("REQUEST_METHOD");
    if (s == NULL)
        return NULL; /* ? */

    if (strcmp(s, "GET") == 0 || strcmp(s, "get") == 0) {
        s = getenv("QUERY_STRING");
        if (s == NULL)
            return NULL;
        param = my_malloc(strlen(s) + 1, "check_env");
        strcpy(param, s);
    } else if (strcmp(s, "POST") == 0 || strcmp(s, "post") == 0) {
        int i;
        int len;

        s = getenv("CONTENT_LENGTH"); /* パラメータの長さ */

        if (s == NULL)
            return NULL;
        len = atoi(s);
        if (len < 1)
            return NULL;

        param = my_malloc(len + 1, "check_env");

        for (i = 0; i < len; i++) {
            param[i] = getchar(); /* freadでも読めそうだが… */
        }

        param[len] = '\0';
    } else {
        return NULL;
    }

    return param;
}

/*--------------------------------------------------------------------*/
/* 文字列paramからnameに該当するフィールドを探して整数値とみなした値を
 * valueの指す先に格納する。
 * 見つかった場合は0、見つからない場合は-1を戻す
 */
static int param_to_int(char *param, char *name, int *value)
{
    char *s;
    int len;

    if (param == NULL || name == NULL || value == NULL)
        return -1;

    s = param;
    len = strlen(name);

    while (*s != '\0') {
        if (strncmp(s, name, len) == 0 && s[len] == '=') {
            *value = atoi(s + len + 1);
            return 0;
        }

        s = strchr(s, '&');
        if (s == NULL)
            break;
        s++;
    }

    return -1;
}
-----------------------------------------------------------------------

1行掲示板

掲示板の超簡単な例である。掲示板も、アクセスカウンタと同様、複数の人が書き込むことのないように注意しなければならない。ただし、今回作るのは1行掲示板である。巷によくある1行掲示板というのは、書き込んだ人それぞれに対して1行しか書けないという制限だが、今回作ろうとしているのは、全員ひっくるめて1行しか書けないという文字通り1行しかない伝言版である。従って、ま、何というか、殆ど役には立たないかもしれないのだが、そういう無茶な仕様も面白いかと思ったので作ってみたのである。

最後に書いた人しか掲示されないというのは仕様としても、あまりに短時間だと可哀想という話もあるから、一定時間は掲示されることを保証する。すなわち、最後に誰かが書き込んだらその時刻も記憶しておき、そこから一定時間経過しない場合は書き込めないということにする。とりあえず5分程度待つという仕様になっている。

---- fig (完成イメージ) ----
1行掲示板
-----------------------------------------
えーっと、何か書けたみたいです。どうも。

メッセージ登録時刻: 1997-05-03 22:34
次回更新可能時刻 1997-05-03 22:39

アイデアとしては、次のような感じになる。

 登録した時刻から一定時間経過していない場合は、登録用フォームのない形式のHTML文書を生成してブラウザに引き渡す。

 登録した時刻から一定時間経過していれば、登録用フォームのあるHTML文書を生成してブラウザに引き渡す。

 登録用フォームがあった場合、ブラウザから何か入力して「登録」ボタンを押したら登録処理のCGIを実行する。その結果、登録できたら画面を更新する。登録できない場合は、残念でした、先を越されました、と表示して、他の人のメッセージで画面を更新してしまう。

従って、この掲示板ページは二段構えのCGIということになる。

まず、最初のリクエストで、表示する画面をダイナミックに生成する必要がある。つまり、データはどこか別のファイルに入っているわけだから、それを埋め込んだドキュメントを作る必要がある。また、最後に更新した時刻を処理して、次回更新可能時刻を表示する必要がある。これらの処理は、コメント形式に埋め込んだタグを使ってSSIでいきなり実行しても構わないが、今回はアンカータグを使って「1行掲示板のコーナー」という個所をクリックしたら画面が出る、というような構成にする。すなわち、どこかのページに、

<A HREF="/cgi-bin/oneline1.cgi>1行掲示板のコーナー</A>

のような感じの行を埋め込んでおき、ブラウザからはここをクリックしたらCGIが起動するように作っておく。

これにより生成されたページがまず表示される。このページにフォームがあれば、それをsubmitした時点でさらにCGIプログラムが呼び出される。

---- fig ----
    ブラウザ                               サーバ

             →   oneline1.cgi を要求  →

             ←     HTMLを出力         ←

             →   formの cgiを実行要求 →

             ←     HTMLを出力         ←

最初に呼ばれるプログラムはListのようになる。この中で、ファイルをフルパスで指定しているが、相対パスで書いたら動作が怪しい場合にこのように書くのがその場しのぎの技である。ところで、実はこのようなプログラムは、本来見せるべきものではない。

1行伝言版のようなファイルは、その性質上、誰でも書けるような設定にする必要がある。そのようなファイルの名前を具体的に示してしまうと、誰がそこに書きに来るか分らない。ということは、もしかすると知らない人が全く別の目的で勝手にこのファイルを使ってしまうかもしれないのである。もちろん、そのような行為は極めてインモラルなことであるが、クラッキングは現にあるわけだから、防衛という意味ではこのようなソースをあまり外部に出さない方が安全なのである。もちろん、本気でクラックしようとする人は、CGIプログラムを直接ダウンロードして逆アセンブルして解析しようとするかもしれない。だったら実行プログラムはパーミッションをxだけオープンしておき、rwは拒否するように設定すべきなのだが、サーバによっては実行ファイルのr権限もオープンでないと実行できない場合があるとかで、話は単純でなさそうである。

このあたりの話題は、WWWでセキュリティ絡みのページを探すと出てくるので、オンラインで探してみて欲しい。

---- List 最初に呼ばれるCGI (oneline1.c) ----
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>

/* 1行メッセージを保存するファイル */
static char *file =
    "/home/usr15/phinloda/public_html/oneline.tmp";
static char *nextcgi =
    "http://www.st.rim.or.jp/~phinloda/cgi-bin/oneline2.cgi";
#define MYBUFSIZE 256
static char mybuf[MYBUFSIZE];

/*--------------------------------------------------------------------*/
int main(int argc, char *argv[])
{
    FILE *fp;
    time_t limit_time = 0;
    time_t current_time;
    struct stat st;
    struct tm *t;

    /* 現在時刻を求める */
    current_time = time(NULL);

    /* ヘッダを生成する */
    printf("Content-type: text/html\n");
    printf("\n");
    printf("<html>\n");
    printf("<head><title>1行掲示板</title></head>\n");
    printf("<body>\n");

    printf("<H1>1行掲示板</H1>\n");
    printf("<hr>\n");

    fp = fopen(file, "r");
    if (fp == NULL) {
        printf("(掲示はありません)\n");
    } else {
        fgets(mybuf, MYBUFSIZE, fp);
        printf("%s", mybuf);
        fclose(fp);
        printf("\n<br>\n");

        if (stat(file, &st) == 0) {
            t = localtime(&st.st_ctime);
            printf("最終更新時刻: %04d-%02d-%02d %02d:%02d:%02d<br>\n",
                t->tm_year+1900, t->tm_mon+1, t->tm_mday,
                t->tm_hour, t->tm_min, t->tm_sec);
            printf("<br>\n");
            limit_time = st.st_ctime + 300; /* 5分 */
            if (limit_time > current_time) {
                t = localtime(&limit_time);

                printf("次回更新は: %04d-%02d-%02d %02d:%02d:%02d",
                    t->tm_year+1900, t->tm_mon+1, t->tm_mday,
                    t->tm_hour, t->tm_min, t->tm_sec);
                printf("以降に可能になります<br>\n");
            } else {
                limit_time = 0;
            }
        } else {
            /* ファイルがあるのにstatがエラーの場合には更新保留 */
            limit_time = -1;
        }
    }
    printf("\n<p>\n");

    if (limit_time == 0) { /* 更新可能 */

        printf("<FORM ACTION=\"%s\" METHOD=\"POST\">\n", nextcgi);
        printf("伝言をどうぞ。<br>\n");
        printf("<INPUT TYPE=\"text\" NAME=\"name\" VALUE=\"(伝言)\" SIZE=\"80\">\n");
        printf("<INPUT TYPE=\"submit\" VALUE=\"登録\">\n");
        printf("</FORM>\n");
    }

    printf("</body></html>\n");

    return 0;
}

ここで、日本語をどう扱うかという大きな問題に悩むことになる。なぜだか現在日本語はいわゆるJIS、SJIS、EUCの三種類が混在しているという困った状況になっている。テキストを入力する時に、利用者はこのどれを使って書いてくるか分らない。Netscape Navigatorのようなブラウザには、日本語を自動判別する機能があるので、たいていのページは適切な表示を行ってくれるが、残念ながら同じペーシの中にJIS、SJIS、EUCが混在したりすると、流石にどうしようもない。この問題を解決する方法は、テキストが入力されたら、とにかくその場でどれか一種類のコードに変換するということになる。

二度目に呼ばれるCGIプログラムはListのようになる。この中ではインチキをやっていて、system関数でnkfを呼び出して文字変換しているのだが、本来は文字コードの変換もCのプログラムで書いてしまいたい所だ。しかし、問題は文字コードの自動判別のアルゴリズムである。完璧な自動判別は不可能なので、どこかで妥協するしかない。安易な発想としては、何か特別な文字を入力させて、それで判別するという方法がある。例えば「漢字」と入力してください、というようなテキストボックスを余計に作っておき、そこに入った値を調べてどの文字か判別する、というちょっと利用者に手間をかけさせる方法なら文字種の判断は確実になる。

関数check_message()は、SSIが書かれた時に無効にすることを狙って「<!--」という文字列が含まれていたら「< --」に置き換えてしまう。また、「`」(バッククオート)は、何となくイヤなのでやはり空白に置換してしまう。ここを強化したり、もう少し真面目に判定したりすれば、少しは安全なコードになるはずである。凄く安易な発想だが、1バイト文字を全てJISの2バイト文字に変換してしまうという手がある。こうすれば、SSIが誤って実行されることはないはずだ。ただし、メッセージを書いた人はびっくりするかもしれない。

---- List 二度目に呼ばれるCGI (oneline2.c) ----
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 1行メッセージを保存するファイル */
static char *file = "/home/usr15/phinloda/public_html/oneline.tmp";
static char *log = "/home/usr15/phinloda/public_html/errlog";

/* 最初のCGI、再び */
static char *url = "http://www.st.rim.or.jp/~phinloda/cgi-bin/oneline1.cgi";

#define DEBUG 1

#if DEBUG
FILE *error_fp;
#endif

/*--------------------------------------------------------------------*/
static void *my_malloc(size_t size)
{
    void *p;

    p = malloc(size);
    if (p == NULL) {
        exit(1);
    }
    return p;
}

/*--------------------------------------------------------------------*/
/* JISからEUCへの変換
 */
static void jis_to_euc(unsigned char *s, FILE *fp)
{
	unsigned char c;
	static int in_kanji;

	in_kanji = 0;

	while ((c = *s++) != 0) {
		if (c == 0x1b) {
			if (*s == '$' && s[1] == 'B') {
				in_kanji = 1;
				s += 2;
			} else if (*s == '(' && s[1] == 'B') {
				in_kanji = 0;
				s += 2;
			} else { /* unknown */
			}
			continue;
		}

		if (in_kanji) {
			putc(c | 0x80, fp);
			c = *s++;
			if (c == '\0')
				break;
		}
		putc(c, fp);
	}
}

/*--------------------------------------------------------------------*/
/* SJISの漢字1文字をEUCの漢字1文字に変換
 */
static unsigned int sjtoeuc(unsigned char c1, unsigned char c2)
{
	if (c1 < 0xa0)
		c1 -= 0x70;
	else if (c1 < 0xf0)
		c1 -= 0xb0;
	else {
		return (c1 << 8) | c2; /* bad range */
	}

	if (c2 >= 0x80)
		c2--;
	c1 *= 2;
	if (c2 >= 0x9e)
		c2 -= 0x5e;
	else
		c1--;
	c2 -= 0x1f;

	return (c1 << 8) | c2 | 0x8080;
}

static int check_sj(unsigned char *s)
{
	unsigned char c;

	while ((c = *s++) != 0) {
		if (c >= 0x81 && ((c <= 0x9f) || (c >= 0xe0 && c <= 0xfc))) {
			c = *s++;
			if (c == '\0') {
				return 0;
			}
		} else if (c >= 0x7f) {
			return 0;
		}
	}

	return 1;
}

static void sj_to_euc(unsigned char *s, FILE *fp)
{
	unsigned char c;

	while ((c = *s++) != 0) {
		if (c >= 0x81 && ((c <= 0x9f) || (c >= 0xe0 && c <= 0xfc))) {
			unsigned char c2;
			unsigned int ui;

			c2 = *s++;
			if (c2 == '\0') {
				putc(c, fp);
				break;
			}
			ui = sjtoeuc((unsigned int ) c, (unsigned int) c2);
			putc(((ui >> 8) & 0xff), fp);
			putc(ui & 0xff, fp);
		} else { /* not kanji */
			if (c != 0x0d) { /* skip CR */
				putc(c, fp);
			}
		}
	}
}

/*--------------------------------------------------------------------*/
void convert_to_euc(char *s, char *filename) {
	FILE *fp;

	fp = fopen(filename, "w");
	if (fp == NULL)
		return;

	if (strchr(s, 0x1b)) {
		jis_to_euc((unsigned char *) s, fp);
	} else if (check_sj((unsigned char *) s)) {
		sj_to_euc((unsigned char *) s, fp);
	} else {
		fputs(s, fp);
	}

	fclose(fp);
}

/*--------------------------------------------------------------------*/
static char *quote_message(char *top)
{
	char *newtop;
    char *p;
    char *s;
	int add = 0;

	s = top;
	while (*s) {
		if (*s == '<') { /* &lt; */
			add += 3;
		} else if (*s == '>') { /* &gt; */
			add += 3;
		} else if (*s == '&') { /* &amp; */
			add += 4;
		} else if (*s == '"') { /* &quot */
			add += 5;
		}
		s++;
	}

	p = newtop = my_malloc(strlen(top) + add + 1);
	s = top;
	while (*s) {
		if (*s == '<') { /* &lt; */
			*p++ = '&';
			*p++ = 'l';
			*p++ = 't';
			*p++ = ';';
		} else if (*s == '>') { /* &gt; */
			*p++ = '&';
			*p++ = 'g';
			*p++ = 't';
			*p++ = ';';
		} else if (*s == '&') { /* &amp; */
			*p++ = '&';
			*p++ = 'a';
			*p++ = 'm';
			*p++ = 'p';
			*p++ = ';';
		} else if (*s == '"') { /* &quot */
			*p++ = '&';
			*p++ = 'q';
			*p++ = 'u';
			*p++ = 'o';
			*p++ = 't';
			*p++ = ';';
		} else {
			*p++ = *s;
		}
		s++;
	}

	return newtop;
}

/*--------------------------------------------------------------------*/
/* 環境変数をチェックして、CGIの引数を得る。
 * 結果はmallocした領域にコピーし、その先頭アドレスを返す。
 * 失敗した場合はNULLを返す。
 */
static char *check_environment(void)
{
    char *s;
    char *param;
    int i;

    s = getenv("REQUEST_METHOD");
    if (s == NULL)
        return NULL; /* ? */

    if (strcmp(s, "GET") == 0 || strcmp(s, "get") == 0) {
        s = getenv("QUERY_STRING");
        if (s == NULL)
            return NULL;
        param = my_malloc(strlen(s) + 1);
        strcpy(param, s);
    } else if (strcmp(s, "POST") == 0 || strcmp(s, "post") == 0) {
        int len;

        s = getenv("CONTENT_LENGTH"); /* パラメータの長さ */

        if (s == NULL)
            return NULL;
        len = atoi(s);
        if (len < 1)
            return NULL;

        param = my_malloc(len + 1);

        for (i = 0; i < len; i++) {
            param[i] = getchar();
        }
        param[i] = '\0';
    } else {
        return NULL;
    }

    return param;
}

/*--------------------------------------------------------------------*/
static char *search_value(char *s)
{
    char *p;

    for (;;) {
        if ((strncmp(s, "name=", 5) == 0) ||
            (strncmp(s, "NAME=", 5) == 0)) {
            p = s + 5;
            break;
        }
        p = strchr(s, '&');
        if (p == NULL) {
            return NULL;
        }
        s = p + 1;
    }

    /* pに、値の文字列の先頭アドレスが入っている */

    s = strchr(p, '&');
    if (s != NULL) {
        *s = '\0';
    }

    return p;
}

/*--------------------------------------------------------------------*/
static void decode(char *value)
{
    int i, j;

    for (i = j = 0; value[i] != '\0'; i++, j++) {
        if (value[i] == '+') {
            value[j] = ' ';
        } else if (value[i] == '%') {
            unsigned char c;

            i++;
            c = value[i];
            if (c == '\0') {
                value[j] = '%'; /* ? */
                value[j+1] = '\0';
                break;
            } else if (c >= '0' && c <= '9') {
                c -= '0';
            } else if (c >= 'A' && c <= 'F') {
                c -= ('A' - 10);
            } else if (c >= 'a' && c <= 'f') {
                c -= ('a' - 10);
            }
            value[j] = c * 16;

            i++;
            c = value[i];
            if (c == '\0') {
                value[j] = '%'; /* ? */
                value[j+1] = '\0';
                break;
            } else if (c >= '0' && c <= '9') {
                c -= '0';
            } else if (c >= 'A' && c <= 'F') {
                c -= ('A' - 10);
            } else if (c >= 'a' && c <= 'f') {
                c -= ('a' - 10);
            }
            value[j] += c;
        } else {
            value[j] = value[i];
        }
    }
	value[j] = '\0';
}

/*--------------------------------------------------------------------*/
int main(int argc, char *argv[])
{
    char *s;
	char *q;

#if DEBUG
	error_fp = fopen(log, "w");
#endif

    s = check_environment(); /* quary文字列を得る */
#if DEBUG
	if (error_fp) {
		fprintf(error_fp, "query = (%s)\n", s);
	}
#endif
    if (s != NULL) {
        char *value;

        value = search_value(s); /* value= に続く文字列を受け取る */
        if (value != NULL) {
#if DEBUG
			if (error_fp) {
				fprintf(error_fp, "value = (%s)\n", value);
			}
#endif
            decode(value); /* %nnを1バイト化 */
#if DEBUG
			if (error_fp) {
				fprintf(error_fp, "decode = (%s)\n", value);
			}
#endif
            q = quote_message(value); /* 危険を避ける */
#if DEBUG
			if (error_fp) {
				fprintf(error_fp, "quote = (%s)\n", q);
			}
#endif
			convert_to_euc(q, file); /* euc文字列にして出力する */
			free(q);
        }
        free(s);
    }

    printf("Location: %s\n\n", url);

#if DEBUG
	fclose(error_fp);
#endif

    return 0;
}

フォームの落とし穴

このように、フォームを使えば、訪問者リストのようなサービスを実現することが可能である。ただし、フォームを作る時にはセキュリティの問題が必ず発生するので、それをよく考えておく必要がある。

訪問者リストには普通の文章だけでなく、HTMLのタグを書く人も出てくる。タグを書くことができれば、自己紹介の中に自分のページへのリンクを書くことができるので、これは便利でもある。ところが、何でも書けるようにしてしまうと、感想の中にSSIを書いてしまう人がいるかもしれない。

<!--#exec cmd="/bin/rm -rf ~"-->

のようなことを書かれたら、それを表示する時に何が起きるかという話である。こういうことを書かれる事態も想定しなければならないのである。

-----------------------------------------------------------------------

ランダムセレクタ

登録してあるURLからランダムに選択する機能である。

通常、ページに登録されているリンク先へジャンプするには、どれか指定してそこに飛ぶというような手順になるが、検索サービスなどでは「どこかに飛ぶ」というようなメニューを見かけることがある。これはCGIのサーバリダイレクション機能を使えば極めて簡単に実現できる。

HTML側は特に何もすることがない。最も簡単なやり方は、アンカーとしてCGIを指定するというものである。次のような1行を追加するだけである。

<A HREF="./cgi-bin/random.cgi">どこか飛ぶ</A>

問題はrandom.cgiの中身である。あらかじめ、飛び先を登録しておき、乱数を使ってURLを選択する。選択したURLは、サーバリダイレクションを使えるように、標準出力に対してLocationを指定する内容として出力する。エラーが発生した時には、とりあえずエラーが発生したという表示をContent-type: text/htmlの形式で出力しておく。このように配慮しないと、利用者にとっては何が起きたか分らない。

このプログラムは、ランダムに飛ぶためのリストをあらかじめファイルとして用意しておき、そこから選択するようになっている。このリストは誰でも見えるように設定しておかなければならない。

---- List (ランダムセレクタ) ----
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

struct list {
    struct list *next;
    char *url;
};

struct list *top = NULL;

#define MY_BUFSIZE 256
static char mybuf[MY_BUFSIZE]; /* こんな長いURLは使わないと思うが… */
static char *database = "/home/usr15/phinloda/public_html/random.dat";

/*--------------------------------------------------------------------*/
void cgi_error(char *s)
{
    printf("Content-type: text/html\n");
    printf("\n");
    printf("<html>\n");
    printf("<head>\n");
    printf("<title>CGI error</title>\n");
    printf("</head>\n");
    printf("<body>\n");

    printf("%s", s);
    printf("</body></html>\n");
    exit(1);
}

/*--------------------------------------------------------------------*/
static void *my_malloc(size_t size)
{
    void *p;

    p = malloc(size);
    if (p == NULL) {
        cgi_error("no memory\n");
        /* exit(1); */
    }
    return p;
}

/*--------------------------------------------------------------------*/
int main(int argc, char *argv[])
{
    FILE *fp;
    char *s;
    int count = 0;
    int i;
    struct list *p;

    fp = fopen(database, "r");
    if (fp == NULL) {
        cgi_error("can't open database\n"); /* exitするため戻ってこない */
    }

    while (fgets(mybuf, MY_BUFSIZE, fp) != NULL) {
        int len;

        len = strlen(mybuf);
        if (len > 0) {
            p = my_malloc(sizeof(struct list));
            /* リストに追加する */
            p->next = top;
            top = p;
            s = my_malloc(len + 1);
            strcpy(s, mybuf);
            top->url = s;
            count++;
        }
    }

    fclose(fp);

    if (count == 0) {
        cgi_error("no URL in database\n");
    }

    /* 乱数を使って適当なURLを選択する */
    srand(time(NULL));
#if 0
    i = (int) ((double)rand() / ((double)RAND_MAX + 1) * count);
#else
	i = rand() % count;
#endif
    for (p = top; i > 0; i--) {
        p = p->next;
    }

    printf("Location: %s\n\n", p->url);

    return 0;
}

-----------------------------------------------------------------------

最新情報ページ

最近更新されているhtmlファイルを集めて、リンクの形でブラウザに戻すというCGIプログラムである。新しいページがあった時に、そこだけ見ることができるので、作ってみたら、自分のページを確認するのにも結構便利だった。

このプログラムは次のような処理を行うだけである。

1) 特定のディレクトリに存在するhtmlファイルの作成時刻を調べる。

2) 調べた結果が、現在時刻と比較して一定の時間内であれば、リストに追加する。

この結果をHTMLの形式にして標準出力に出せばよいだけだ。従って問題は時刻をどうやって得るとか、そこにファイルがあることをどうやって調べるかというような、処理系依存の話になってしまう。

Listは、最近のファイルを検索する処理を行う関数である。htmlファイルの時刻を調べるのに、UNIXローカルのCライブラリ関数、opendir、readdirを呼び出している。このあたりは、UNIXのバージョンなどによって変更が必要かもしれない。現在時刻をtime関数で得て、適当な定数を引き算している。これはtime関数が秒数を戻す場合には7日間という期間になっているが、秒数を戻すかどうかは定かではないのであまり正しいプログラムではないが、今回は見逃してください。

この例では、特定のディレクトリの下にあるファイルの中から、".htm"というファイル名があるものだけを調べている。<ul>〜</ul>タグを使ったリストの中に、目的のファイルが見つかった毎に<li>を添えて情報追加するような仕組みになっている。こうすると、<ul>〜</ul>の中に一つも項目がない場合もあるのだが、特に実害はないようである。

---- List (最近のファイルを検索する関数) ----
static void search_new_html(void)
{
    DIR *dirp;
    char *s;
    int found = 0;
    struct dirent *direntp;
    struct stat st;
    struct tm *t;
    time_t limit_time;

    /* 現在時刻を求める */
    limit_time = time(NULL) - (60 * 60 * 24 * 7);

    printf("<ul>\n");

    dirp = opendir(rootdir);
    while ((direntp = readdir(dirp)) != NULL) {
        if (strstr(direntp->d_name, ".htm")) {
            s = my_malloc(strlen(rootdir) + strlen(direntp->d_name) + 2);
            strcpy(s, rootdir);
            strcat(s, "/");
            strcat(s, direntp->d_name);
            if (stat(s, &st) == 0) {
                if (st.st_ctime > limit_time) {
                    t = localtime(&st.st_ctime);
                    printf("<li><a href=\"%s/%s\">%s/%s</a> ",
						urlpath, direntp->d_name,
						urlpath, direntp->d_name);
                    printf("(%04d-%02d-%02d)",
                        t->tm_year+1900, t->tm_mon+1, t->tm_mday);
                    printf("\n");
                    found++;
                }
            }
            free(s);
        }
    }

    printf("</ul>\n");

    closedir(dirp);

サーチを実行するディレクトリはプログラムの中でstaticに指定している。CGIを呼び出す時に指定するように変更するのは割合簡単である。

-----------------------------------------------------------------------

Perlを使うとどうなるのか

統計では、CGIを作成する時にPerlが使われることが最も多いそうである。Perlというのは"Practical Extraction and Report Language"の頭文字なのだが、文字通りのPerlという意味も掛けているようである。具体的には、テキストファイルを読み込んで処理をするインタープリタである。同様のツールとして、sedやawkが有名であるが、Perlはこれらのプログラムが出来ることなら大抵のことをより効率的に処理できる。すなわち、同じ処理を行うのであれば、単純なPerlスクリプトを使って、多分より短時間で目的を達成できると思われる。

Perlはawkのように文字列の処理が得意だし、しかもUNIXのシステムコールに互換な関数やプロセス間通信などの細かい処理まで実装されている。やろうと思えばバイナリのデータを読み込んで処理できてしまう。これらの特徴はCGIの処理にうってつけだと言える。

では、C言語の出番はないのだろうか。PerlとC、両方を使えるなら、Perlのメリットは捨て難い。特に保守性に関しては、C言語で書いたプログラムよりもPerlの方が柔軟に対応できる。しかし、最もしばしば言われることとして、PerlよりCの方が実行速度が有利という特徴がある。

次のListはランダムセレクタのCGIをPerlで書いたものである。Listを見ると、次のようなことが分る。

1. PerlとCは似ている。細かい所はもちろん異なっているが、構文や予約語の使い方などは、酷似している。だから、C言語が使える人なら、Perlはすぐに使えるようになるはずだ。

2. メモリ管理や変数の管理はPerlに任せることができる。mallocを使って確保しなくても、無頓着に1行ずつデータを読み込んで、配列にどかどか突っ込んでいる。実はこれでメモリが足りなくなったらどうするんだという話もあるわけだが、とりあえず動くという環境さえあれば、問題なく動いてしまうのである。

3. ヒアドキュメント機能が便利。サブルーチンとして定義されているcgi_errorの中の処理を見て欲しい。このように埋め込まれたドキュメントが、そのまま標準出力に出ていく。しかも、変数が書かれている所は、変数の値に置き換えられた結果が出る。C言語でこれをすると、printfやputsのような関数をいちいち呼び出すことになるので割と面倒になる。特にCGIというのは標準出力にHTML形式のデータを出すというのが最終目的になるわけだから、この機能は極めて便利だ。

---- List (ランダムセレクタ Perl版) ----
#!/usr/local/bin/perl
# ランダムセレクタ

$database = "/home/usr15/phinloda/public_html/random.dat";

if (!open(FILE, $database)) {
  &cgi_error("can't open $database");
}

while (<FILE>) {
  $url_list[$count] = $_; # $countという変数をいきなり使うと初期値は0になる
  $count++;
}

close(FILE); # 開いたものは、とりあえず閉じましょう

if ($count == 0) {
  &cgi_error("no URL in database");
}

# 乱数を使って適当なURLを選択する
srand; # かき混ぜる
$i = int(rand($count)); # 乱数は小数なので、整数値に変換しておく
if ($i == $count) { # そんなことあるのか?
  $i--;
}

print "Location: $url_list[$i]\n";  # $url_list[$i] に '\n' が付いている

exit 0; # ここでおしまい。

# --------------------------------------------------------------------
# エラー表示を出して終了するサブルーチン
sub cgi_error {
local($error_message) = @_; # ローカル変数 $error_message を使う
# この2行後からは、標準出力にそのまま書き出される(here document)
print <<CGI_ERROR_EOF;
Content-type: text/html

<html>
<head>
<title>CGI error</title>
</head>
<body>

$error_message
</body></html>
CGI_ERROR_EOF

exit 0;
}

(フィンローダ NIFTY SERVE, FPROG SYSOP)

http://www.st.rim.or.jp/~phinloda/


(C) 1998 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。