CでCGIスクリプトを作ってみよう


イントロダクション

Webで動的にコンテンツを作る仕組みとして、CGI(Common Gateway Interface)がよく使われています。サーブレットなども流行っていますが、私はCGIが好きです。単純でわかりやすいし、仕様が枯れているからです。ここでは、CGIスクリプトをC言語を使って実装する方法について書いてみます。

ところで、CGIはインターフェイスの規約に過ぎないので、「CGIを実装する」というと、WebサーバにCGIスクリプトを呼び出す機能をつけるということになってしまいます。ここで紹介する、CGIを介して呼び出されるプログラムは「CGIスクリプト」と呼ばれます。間違っても、「俺、CGI作れるぜ」と公言するのはやめましょう。とはいえ、社会生活においてそのような趣旨の突っ込みを入れると嫌な顔されることうけあいです。

スクリプトというくらいなので、CGIスクリプトを実装するのにはPerl等のいわゆるスクリプト言語を使うのが一般的なのかもしれません。しかし、Cでも書けます。私はCが趣味なのでCで書きますが、仕事でCGIスクリプトを書くことになって、ソースを公開したくない(バイナリ納品したい)という理由から、Cで書かねばならない人も多いと思います。理由は様々あれど、前向きにやろうではないですか。

サーブレット等の常駐型のインターフェイスの方が、呼び出される度にプロセスを起動するCGIよりも高速に動作するという意見もありますが、実はそうでもないです。PerlのCGIスクリプトとJavaのサーブレットを比べた場合には後者に軍配が上がるかもしれませんが、CのCGIスクリプトなら大抵はJavaのサーブレットに勝てると思います。

CGIスクリプトはWebサーバから呼び出されます。HTMLのフォームなどにユーザが入力した内容は、環境変数や標準入力として渡されます。そして、その内容に応じて処理を行い、標準出力にデータを吐き出します。つまり、環境変数と標準入出力をサポートした環境や言語であれば、CGIスクリプトを実装できるわけです。もちろん、UNIXでもWindowsでもMacでもOKです。

CGIの入力や出力は、CGIの仕様で規定された形式をとります。その形式のデータを加工するのが結構面倒です。文字列やバイナリデータの加工をたんまりとやらねばなりません。いわゆるスクリプト言語では、CGIにまつわるデータ加工を行うライブラリが充実しているから、CGIスクリプトを書きやすいのです。そこで、拙作のQDBMというライブラリには、CGIスクリプトで使われるであろう機能がたくさん入っています。自分でよく使うからです。実はこのページの目的は、QDBMのそういった機能を宣伝することです。


入力パラメータの解析

HTMLに以下のような記述があったとします。掲示板に記事を書き込むような機能でよく見られるパターンです。

<form method="get" action="http://foo.bar.baz/hoge.cgi">
<div>
<input type="text" name="title" value="" />
<input type="text" name="author" value="" />
<input type="submit" value="SUBMIT" />
</div>
<div>
<textarea name="body" rows="3" cols="70">
</textarea>
</div>
</form>

この場合、以下のことがわかります。

GETメソッドの場合、パラメータは環境変数 `QUERY_STRING' の値として渡されます。form要素で `get' と指定している場所を `post' と指定すると、POSTメソッドになります。その場合は、パラメータは標準入力から渡されます。標準入力の長さは、環境変数 `CONTENT_LENGTH' で判断できます。GETかPOSTかは、環境変数 `REQUEST_METHOD' の値で判断できます。いずれにせよ、全てのパラメータはURLエンコード(application/x-www-form-urlencoded)と呼ばれる形式で一つの文字列に連結されて渡されます。これを解析するのが第一の課題です。

例えば、`title' の入力値が `Hello' で、`author' の入力値が `mikio' で、`body' の入力値が「hoge」だとすると、以下のような文字列が渡ってきます。

title=Hello&author=mikio&body=hoge

入力値にスペース `?' や `&' などの記号や、日本語などの多バイト文字がある場合、エスケープされます。多バイト文字はHTMLの文字コードの影響を受けます。`title' の入力値が `Hello' で、`author' の入力値が `Mikio Hirabayashi' で、`body' の入力値が「こんにちは」で、文字コードがEUC-JPだとすると、以下のような文字列が渡ってきます。

title=Hello&author=Mikio%20Hirabayashi&body=%A4%B3%A4%F3%A4%CB%A4%C1%A4%CF

これを元に戻すには、以下の手順を行うことになります。

この手順を簡略化すべく、以下のような関数を作りましょう。データの加工にQDBMが大活躍です。

/* CGIパラメータを読んで、マップとして返す */
CBMAP *getparams(void){
  CBMAP *params;
  CBLIST *pairs;
  char *rbuf, *buf, *key, *val, *dkey, *dval;
  const char *tmp;
  int i, len, c;
  params = cbmapopen();
  rbuf = NULL;
  buf = NULL;
  if((tmp = getenv("CONTENT_LENGTH")) != NULL && (len = atoi(tmp)) > 0){
    rbuf = cbmalloc(len + 1);
    for(i = 0; i < len && (c = getchar()) != EOF; i++){
      rbuf[i] = c;
    }
    rbuf[i] = '\0';
    if(i == len) buf = rbuf;
  } else {
    buf = getenv("QUERY_STRING");
  }
  if(buf != NULL){
    buf = cbmemdup(buf, -1);
    pairs = cbsplit(buf, -1, "&");
    for(i = 0; i < cblistnum(pairs); i++){
      key = cbmemdup(cblistval(pairs, i, NULL), -1);
      if((val = strchr(key, '=')) != NULL){
        *(val++) = '\0';
        dkey = cburldecode(key, NULL);
        dval = cburldecode(val, NULL);
        cbmapput(params, dkey, -1, dval, -1, FALSE);
        free(dval);
        free(dkey);
      }
      free(key);
    }
    cblistclose(pairs);
    free(buf);
  }
  free(rbuf);
  return params;
}

上記を用意しておけば、あとは以下のように、パラメータを取り出せます。

int main(int argc, char **argv){
  CBMAP *params;
  const char *title, *author, *body;

  /* パラメータを解析する */
  params = getparams();

  /* それぞれのデータを取り出す */
  title = cbmapget(params, "title", -1, NULL);
  author = cbmapget(params, "author", -1, NULL);
  body = cbmapget(params, "author", -1, NULL);

  /* 出力処理をする */
  printf("Content-Type: text/html\r\n\r\n");
  /*
   * その他もろもろの処理
   */

  /* パラメータの領域を開放する */
  cbmapclose(params);

  return 0;
}

ということで、C言語でも(QDBMを使えば)CGIを楽勝に書けることがお分かりいただけたかと思います。


出力の加工

Webインターフェイスのアプリケーションを作っていて問題となるのが、いわゆるクロスサイトスクリプティングです。ユーザが入力した文字列をそのまま表示してしまうと、HTMLのタグやJavaScriptの命令を入力された時に、ブラウザが予期せぬ挙動をしてしまうのです。それで情報漏洩など起こされたら、サラリーマン人生が終わってしまうことになりかねません。

ユーザの入力に由来する文字列を表示する場合は、HTMLのメタ文字 `&'、'<'、`>'、`"' をそれぞれ `&amp;'、'&lt;'、`&gt;'、`&quot;' にエスケープすることが必要です。また、URLの一部として出力する場合は、URLに含めてはいけない文字(URLエンコードで述べたものと同じ)をエスケープする必要があります。

ところで、`printf' という関数はすごい便利ですよね。これと同じノリで、エスケープと出力を同時にやってしまう `xprintf' という関数を作りましょう。書式文字列に表れる `%s' と `%d' は printf と同じように機能します。さらに、`%@' とすると、対応する引数をHTMLエスケープした上で出力します。`%?' とすると、対応する引数をURLエンコードした上で出力します。

/* HTMLエスケープ機能つきのprintf */
void xprintf(const char *format, ...){
  va_list ap;
  char *tmp;
  unsigned char c;
  va_start(ap, format);
  while(*format != '\0'){
    if(*format == '%'){
      format++;
      switch(*format){
      case 's':
        tmp = va_arg(ap, char *);
        if(!tmp) tmp = "(null)";
        printf("%s", tmp);
        break;
      case 'd':
        printf("%d", va_arg(ap, int));
        break;
      case '@':
        tmp = va_arg(ap, char *);
        if(!tmp) tmp = "(null)";
        while(*tmp){
          switch(*tmp){
          case '&': printf("&amp;"); break;
          case '<': printf("&lt;"); break;
          case '>': printf("&gt;"); break;
          case '"': printf("&quot;"); break;
          default:
            if(!((*tmp >= 0 && *tmp <= 0x8) || (*tmp >= 0x0e && *tmp <= 0x1f))) putchar(*tmp);
            break;
          }
          tmp++;
        }
        break;
      case '?':
        tmp = va_arg(ap, char *);
        if(!tmp) tmp = "(null)";
        while(*tmp){
          c = *(unsigned char *)tmp;
          if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
             (c >= '0' && c <= '9') || (c != '\0' && strchr("_-.", c))){
            putchar(c);
          } else {
            printf("%%%02X", c);
          }
          tmp++;
        }
        break;
      case '%':
        putchar('%');
        break;
      }
    } else {
      putchar(*format);
    }
    format++;
  }
  va_end(ap);
}

使い方は、以下のようになります。

xprintf("<p>ようこそ「%@」さん。</p>", author);

出力する際には、必ずこの `xprintf' を使うことが重要です。このクセをつけておけば、クロスサイトスクリプティングからはオサラバです。


ファイルアップロード

最後に、CGIスクリプターの鬼門である、ファイルアップロードに取り組みましょう。以下のようなフォームで、クライアントのコンピュータにあるファイルをアップロードする機能がつきます。

<form method="post" enctype="multipart/form-data" action="http://saitama.go.jp/10mangoku.cgi">
<div>
<input type="file" name="myfile" size="64" class="file" tabindex="12" />
<input type="submit" value="SUBMIT" />
</div>
</form>

ファイルアップロードを行うには、メソッドがPOSTであることと、フォームのエンコード指定(enctype)がmultipart/form-dataであることが必要です。そして、サーバ側ではそれに対応した解析処理を行わねばなりません。multipart/form-dataというのは電子メールの添付ファイルで使うmultipart/alternativeなどの仲間です。application/x-www-form-urlencodedでは `&' でパーツを区切っていましたが、multipart/xxx の場合は、「---------hogehoge-1234」のような形をしたバウンダリ文字列というもので区切ります。さらに、各パートの先頭に `Content-Disposition' というヘッダがついて、元のファイル名などの情報が渡されます。他にもいろいろ機能はありますが、一般的なブラウザではそれくらいしか使われていません。

さて、通常(application/x-www-form-urlencoded)の場合でもファイルアップロード(multipart/form-data)の場合でも対応できるように、既に紹介した `getparams' 関数を改造しましょう。

/* アップロードできる最大サイズ */
#define RDATAMAX  67108864

/* CGIパラメータを読んで、マップとして返す(アップロード対応版) */
CBMAP *getparameters(void){
  CBMAP *map, *attrs;
  CBLIST *pairs, *parts;
  const char *tmp, *body;
  char *buf, *key, *val, *dkey, *dval, *wp, *bound, *fbuf, *aname;
  int i, len, c, blen, flen;
  map = cbmapopen();
  buf = NULL;
  len = 0;
  if((tmp = getenv("REQUEST_METHOD")) != NULL && !strcmp(tmp, "POST") &&
     (tmp = getenv("CONTENT_LENGTH")) != NULL && (len = atoi(tmp)) > 0){
    if(len > RDATAMAX) len = RDATAMAX;
    buf = cbmalloc(len + 1);
    for(i = 0; i < len && (c = getchar()) != EOF; i++){
      buf[i] = c;
    }
    buf[i] = '\0';
    if(i != len){
      free(buf);
      buf = NULL;
    }
  } else if((tmp = getenv("QUERY_STRING")) != NULL){
    buf = cbmemdup(tmp, -1);
    len = strlen(buf);
  }
  if(buf && len > 0){
    if((tmp = getenv("CONTENT_TYPE")) != NULL && cbstrfwmatch(tmp, "multipart/form-data") &&
       (tmp = strstr(tmp, "boundary=")) != NULL){
      tmp += 9;
      bound = cbmemdup(tmp, -1);
      if((wp = strchr(bound, ';')) != NULL) *wp = '\0';
      parts = cbmimeparts(buf, len, bound);
      for(i = 0; i < cblistnum(parts); i++){
        body = cblistval(parts, i, &blen);
        attrs = cbmapopen();
        fbuf = cbmimebreak(body, blen, attrs, &flen);
        if((tmp = cbmapget(attrs, "NAME", -1, NULL)) != NULL){
          cbmapput(map, tmp, -1, fbuf, flen, FALSE);
          aname = cbsprintf("%s-filename", tmp);
          if((tmp = cbmapget(attrs, "FILENAME", -1, NULL)) != NULL)
            cbmapput(map, aname, -1, tmp, -1, FALSE);
          free(aname);
        }
        free(fbuf);
        cbmapclose(attrs);
      }
      cblistclose(parts);
      free(bound);
    } else {
      pairs = cbsplit(buf, -1, "&");
      for(i = 0; i < cblistnum(pairs); i++){
        key = cbmemdup(cblistval(pairs, i, NULL), -1);
        if((val = strchr(key, '=')) != NULL){
          *(val++) = '\0';
          dkey = cburldecode(key, NULL);
          dval = cburldecode(val, NULL);
          cbmapput(map, dkey, -1, dval, -1, FALSE);
          free(dval);
          free(dkey);
        }
        free(key);
      }
      cblistclose(pairs);
    }
  }
  free(buf);
  return map;
}

なお、アップロードした内容を全てメモリ上で保持しているため、あまりに巨大なファイルがアップロードされると困ってしまう仕様になっています。そのため、`RDATAMAX' というマクロで最大サイズを制限しています(これが嫌な場合は、途中でファイルに書き出すようにしないといけません)。

使い方は元のバージョンとほぼ同じです。`myfile' というパーツのデータを取り出したい場合は、マップから `myfile' を取り出します。その際に、ファイルがバイナリデータの場合に備えて、サイズを格納する変数へのポインタも同時に渡します。また、`myfile' の元のファイル名が知りたい場合は、`myfile-filename' を取り出します。

int main(int argc, char **argv){
  CBMAP *params;
  const char *myfiledata, *myfilename;
  int myfilesize;

  /* パラメータを解析する */
  params = getparameters();

  /* それぞれのデータを取り出す */
  myfiledata = cbmapget(params, "myfile", -1, &myfilesize);
  myfilename = cbmapget(params, "myfile-filename", -1, NULL);

  /* 出力処理をする */
  printf("Content-Type: text/html\r\n\r\n");
  /*
   * その他もろもろの処理
   */

  /* パラメータの領域を開放する */
  cbmapclose(params);

  return 0;
}

以上です。これであなたもCとCGIとQDBMのトリコ。