CGI スクリプト

(警告: 適切なセーフガードをほどこしていない CGI スクリプトはサイトのセキュリティを危うくします。ここにある スクリプトは単純な例であって、実際のウェブで利用を前提とする セキュリティを考えていません。)

CGI スクリプト [cite{cgi}] はウェブサーバに置かれたスクリプトで クライアント(ブラウザ)から起動することができます。 クライアントは通常のページと全くおなじく、URL によって CGI スクリプトにアクセスします。サーバはリクエストを受けた URL が CGI スクリプトであることを認識し、それを走らせます。サーバが どうやって、特定の URL をスクリプトとして認識するかはサーバの 管理者しだいです。このテキストではスクリプトは専用の cgi-bin というディレクトリに置かれていると仮定しています。したがって、 サーバ www.foo.org 上のスクリプト testcgi.scmhttp://www.foo.org/cgi-bin/testcgi.scm としてアクセスされる ことになります。

サーバは CGI スクリプトを nobody という PATH について なにも知らないはず(というのはたいへん主観的な判断です)のユーザとして 走らせます。それ故に、Scheme で書いた CGI スクリプトの前置きの 魔法の行は、一般の Scheme スクリプトより少しだけ明示的に書くものが 増えます。たとえば、

":";exec mzscheme -r $0 "$@"

という行は暗黙にある特定のシェル(たとえば bash) と PATH とそのなかに mzscheme があることを仮定しています。 CGI スクリプトでは以下のように、より明示的に書く必要があります。

#!/bin/sh
":";exec /usr/local/bin/mzscheme -r $0 "$@"

これはシェルと Scheme のフルパスを与えています。シェルから Scheme への制御の受渡しは通常のスクリプトと同じです。

17.1  例: 環境変数の表示

さて、Scheme CGI スクリプトの例です。testcgi.scm は CGI 環境変数で使われるもののいくつかの設定を出力するスクリプトです。 この情報は、新しく作られたページとしてブラウザに返されます。 返されるページは単に CGI スクリプトが標準出力に出したものです。 これが CGI スクリプトがそれを呼び出したところへ応える方法です。 これは新しいページを返すことでおこなわれます。

このスクリプトは最初に、

content-type: text/plain 

という行を出力し、続いて空行を出力するということに 注意してください。これはウェブサーバがページをサービスする 標準的な儀礼です。この二つの行は実際にページとして表示されるものの 部分ではありません。ブラウザに送られてくるページが (マークアップされていない)プレーンテキストであることを知らせるための ものです。これによりブラウザは正しく表示ができます。テキストを HTML でマークアップしたのなら、content-typetext/html と なるでしょう。

スクリプト textcgi.scm は以下のようになります。

#!/bin/sh
":";exec /usr/local/bin/mzscheme -r $0 "$@"

;content-type がプレーンテキストであることを確認

(display "content-type: text/plain") (newline)
(newline)

;要求された情報を含むページを生成
;これは単に標準出力に書き出すだけ

(for-each
 (lambda (env-var)
   (display env-var)
   (display " = ")
   (display (or (getenv env-var) ""))
   (newline))
 '("AUTH_TYPE"
   "CONTENT_LENGTH"
   "CONTENT_TYPE"
   "DOCUMENT_ROOT"
   "GATEWAY_INTERFACE"
   "HTTP_ACCEPT"
   "HTTP_REFERER" ; [sic]
   "HTTP_USER_AGENT"
   "PATH_INFO"
   "PATH_TRANSLATED"
   "QUERY_STRING"
   "REMOTE_ADDR"
   "REMOTE_HOST"
   "REMOTE_IDENT"
   "REMOTE_USER"
   "REQUEST_METHOD"
   "SCRIPT_NAME"
   "SERVER_NAME"
   "SERVER_PORT"
   "SERVER_PROTOCOL"
   "SERVER_SOFTWARE"))

testcgi.scm はブラウザ上で開くことで直接呼び出すことができます。 その URL は以下のとおりです。

http://www.foo.org/cgi-bin/testcgi.scm 

そのほかに、testcgi.scm は HTML ファイル内のリンクとしても 出現可能で、これをクリックすることもできます。

... 一般的な CGI 環境変数のいくつかを見るためには、 
<a href="http://www.foo.org/cgi-bin/testcgi.scm">ここ</a> 
をクリックしてください。 
... 

これでも、testcgi.scm は起動され、環境変数の設定を含む プレーンテキストのページが作られます。たとえば、以下のような 出力になります。

AUTH_TYPE = 
CONTENT_LENGTH = 
CONTENT_TYPE = 
DOCUMENT_ROOT = /home/httpd/html 
GATEWAY_INTERFACE = CGI/1.1 
HTTP_ACCEPT = image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */* 
HTTP_REFERER = 
HTTP_USER_AGENT = Mozilla/3.01Gold (X11; I; Linux 2.0.32 i586) 
PATH_INFO = 
PATH_TRANSLATED = 
QUERY_STRING = 
REMOTE_HOST = 127.0.0.1 
REMOTE_ADDR = 127.0.0.1 
REMOTE_IDENT = 
REMOTE_USER = 
REQUEST_METHOD = GET 
SCRIPT_NAME = /cgi-bin/testcgi.scm 
SERVER_NAME = localhost.localdomain 
SERVER_PORT = 80 
SERVER_PROTOCOL = HTTP/1.0 
SERVER_SOFTWARE = Apache/1.2.4 

17.2  例: 選択された環境変数の表示

testcgi.scm はユーザからは何の入力も受けません。ユーザからの 環境変数を指定する引数を受け、その変数の設定を表示し他は表示しない スクリプトを考えましょう。このためには、CGI スクリプトに引数を 食わせるための機構が必要になります。 HTML の form タグはこれを可能にします。以下はその サンプルです。

<html> 
<head> 
<title>環境変数をチェックするフォーム</title> 
</head> 
<body> 
 
<form method=get  
      action="http://www.foo.org/cgi-bin/testcgi2.scm"> 
環境変数を入力してください: <input type=text name=envvar size=30> 
<p> 
 
<input type=submit> 
</form> 
 
</body> 
</html> 

ユーザが知りたい環境変数(たとえば、GATEWAY_INTERFACE)を テキストボックスに入力し、subimt ボタンをクリックします。 そうするとフォーム中の情報 — ここでは、envvar というパラメータに GATEWAY_INTERFACE という値がセットされ — が集められ form によって指定された CGI スクリプト、すなわち testcgi2.scm に渡されます。この情報は二通りの方法のどちらかで送られます。 (1) formmethod=get (デフォルト)であれば、情報は QUERY_STRING という環境変数を通じて送られます。(2) formmethod=post なら、情報はスクリプトの標準入力ポート(stdin) から 利用可能になります。ここのフォームでは QUERY_STRING を使います。

QUERY_STRING から情報をとりだし、それに応じた回答のページを 出力する責任は testcgi2.scm にあります。

CGI スクリプトへの情報は、環境変数経由であろうと、stdin 経由であろうと、パラメータ/引数の対の並びに整形されています。 この対は、文字 & で区別されています。対の中では、パラメータは 最初に出現し、文字 = で、その引数と区別されています。 この場合には、パラメータ/引数の対はひとつだけで、 envvar=GATEWAY_INTERFACE です。

スクリプト testcgi2.scm は以下の通りです。

#!/bin/sh
":";exec /usr/local/bin/mzscheme -r $0 "$@"

(display "content-type: text/plain") (newline)
(newline)

;string-index は文字列 s の一番左にあらわれる 文字 c の
;インデックスを返す

(define string-index
  (lambda (s c)
    (let ((n (string-length s)))
      (let loop ((i 0))
        (cond ((>= i n) #f)
              ((char=? (string-ref s i) c) i)
              (else (loop (+ i 1))))))))

;split は文字列 s を文字 c で区分された部分文字列に分割する

(define split
  (lambda (c s)
    (let loop ((s s))
      (if (string=? s "") '()
          (let ((i (string-index s c)))
            (if i (cons (substring s 0 i)
                        (loop (substring s (+ i 1)
                                         (string-length s))))
                (list s)))))))

(define args
  (map (lambda (par-arg)
         (split #\= par-arg))
       (split #\& (getenv "QUERY_STRING"))))

(define envvar (cadr (assoc "envvar" args)))

(display envvar)
(display " = ")
(display (getenv envvar))

(newline)

補助手続き split をつかって QUERY_STRING を 文字 & にしたがって、パラメータ/引数の対に分割し、 さらに、それを使って、文字 = にしたがって、パラメータと 引数を分割しているということに注意してください。 (get メソッドの代りに post メソッドをを使うのなら、 パラメータと引数を標準入力から引出す必要があります。)

<input type=text> および <input type=submit> は HTML の form で使えるいろいろな input タグのうちの ふたつにしかすぎません。全部のレパートリーについては [cite{cgi}] を参照してください。

17.3  CGI スクリプトユーティリティ

上の例ではパラメータの名前あるいは仮定されるその引数は、それ自身には ‘&’ あるいは ‘=’ の文字は含んでいませんでした。が、一般的には 含まれる可能性があります。そのような文字に対応し、セパレータと 間違うことのないように、CGI の引数の引き渡し機構は英字、数字、 アンダースコア以外のすべての文字を特別あつかいし、それを エンコード形式で伝達します。空白は ‘+’ にエンコードされます。 そのほかの特殊文字については、エンコードは 3 文字の並びになります。 この 3文字の並びは、‘%’ とその特殊文字の16進コードとなります。 したがって、文字のならび ‘20% + 30% = 50%, \&c.’ は次のように エンコードされます。

20%25+%2b+30%25+%3d+50%25%2c+%26c%2e 

(空白は ‘+’、‘%’ は ‘%25’、 ‘+’ は ‘%2b’、‘=’ は ‘%3d’、 ‘,’ は ‘%2c’、‘&’ は ‘%26’、 そして ‘.’ は ‘%2e’ になっています。)

各 CGI スクリプト中のフォームデータの獲得とデコードについて改めて とりあつかう代わりにいくつかの有用な手続きをライブラリファイル cgi.scm に集めておくのが便利です。そうすると、testcgi2.scm は よりコンパクトに次のように書けます。

#!/bin/sh
":";exec /usr/local/bin/mzscheme -r $0 "$@"

;cgi ユーティリティのロード

(load-relatve "cgi.scm")

(display "content-type: text/plain") (newline)
(newline)

;フォーム経由のデータ入力の読み込み

(parse-form-data)

;envvar パラメータの獲得

(define envvar (form-data-get/1 "envvar"))

;envvar の値の表示

(display envvar)
(display " = ")
(display (getenv envvar))
(newline)

この短い方の CGI スクリプトは、cgi.scm で定義されている、ふたつの ユーティリティ手続きを使っています。parse-form-data は ユーザからフォームをつうじて供給されたデータを読むのに使います。 そのデータはパラメータとそれにむすびついた値から構成されています。 form-data-get/1 は特定のパラメータにむすびついた値を見つけます。

cgi.scm はフォームデータを格納するための *form-data-table* というグローバルテーブルを定義しています。

;テーブルの定義のロード

(load-relative "table.scm")

;*form-data-table* の定義

(define *form-data-table* (make-table 'equ string=?))

parse-form-data 手続きのような一般的機構を使う長所は どの method (get あるいは post) が使われているかという 詳細を隠蔽することができるというものです。

(define parse-form-data
  (lambda ()
    ((if (string-ci=? (or (getenv "REQUEST_METHOD") "GET") "GET")
         parse-form-data-using-query-string
         parse-form-data-using-stdin))))

環境変数 REQUEST_METHOD はフォームデータを転送するのに どのメソッドが使われているかを教えるものです。メソッドが GET の場合、フォームデータはもうひとつの環境変数 QUERY_STRING を通じて正当な文字列として送られます。 補助手続き parse-from-data-using-query-stringQUERY_STRING をばらばらに取り出すために使われます。

(define parse-form-data-using-query-string
  (lambda ()
    (let ((query-string (or (getenv "QUERY_STRING") "")))
      (for-each
       (lambda (par=arg)
         (let ((par/arg (split #\= par=arg)))
           (let ((par (url-decode (car par/arg)))
                 (arg (url-decode (cadr par/arg))))
             (table-put! 
              *form-data-table* par
              (cons arg 
                    (table-get *form-data-table* par '()))))))
       (split #\& query-string)))))

ヘルパ手続き split およびさらにそのヘルパ string-index は 17.2 節のように定義されています。コメントにあるように、 やってきたフォームデータは & で区切られた、名前-値対の並びです。 それぞれの対の中では、名前が先にきて、そのあとに = がきて、さらに 値がきます。それぞれの、名前-値の組み合わせはグローバルテーブル *form-data-table* に集められます。

名前、値ともにエンコードされていますので、url-decode 手続きを 使ってデコードする必要があります。

(define url-decode
  (lambda (s)
    (let ((s (string->list s)))
      (list->string
       (let loop ((s s))
         (if (null? s) '()
             (let ((a (car s)) (d (cdr s)))
               (case a
                 ((#\+) (cons #\space (loop d)))
                 ((#\%) (cons (hex->char (car d) (cadr d))
                              (loop (cddr d))))
                 (else (cons a (loop d)))))))))))

+’ は空白に変換します。3 文字からなる ‘%xy’ という形式は 手続き hex->char を使って ‘xy’ という ASCIIコードをもつ文字に 変換されます。

(define hex->char
  (lambda (x y)
    (integer->char
     (string->number (string x y) 16))))

また、リクエストメソッドが POST である場合のフォームデータ パーザも必要です。補助手続き parse-form-data-using-stdin は次のよう になります。

(define parse-form-data-using-stdin
  (lambda ()
    (let* ((content-length (getenv "CONTENT_LENGTH"))
           (content-length
             (if content-length
                 (string->number content-length) 0))
           (i 0))
      (let par-loop ((par '()))
        (let ((c (read-char)))
          (set! i (+ i 1))
          (if (or (> i content-length)
                  (eof-object? c) (char=? c #\=))
              (let arg-loop ((arg '()))
                (let ((c (read-char)))
                  (set! i (+ i 1))
                  (if (or (> i content-length)
                          (eof-object? c) (char=? c #\&))
                      (let ((par (url-decode
                                   (list->string
                                     (reverse! par))))
                            (arg (url-decode
                                   (list->string
                                     (reverse! arg)))))
                        (table-put! *form-data-table* par
                          (cons arg (table-get *form-data-table*
                                      par '())))
                        (unless (or (> i content-length)
                                    (eof-object? c))
                          (par-loop '())))
                      (arg-loop (cons c arg)))))
              (par-loop (cons c par))))))))

POST メソッドはフォームデータをスクリプトと stdin 経由で送ります。送られた文字の数は、環境変数 CONTENT_LENGTH に あります。parse-form-data-using-stdin は必要な文字数分だけ stdin から文字を読みます。そして、前とおなじように、 パラメータの名前と値をデコードしたうえで、*form-data-table* に 書きこみます。

*form-data-table* から特定のパラメータの値を取ってくるのが まだのこっています。

(define form-data-get
  (lambda (k)
    (table-get *form-data-table* k '())))

form-data-get/1 はパラメータと結びついている最初の (あるいは一番意味のある)値を返します。

(define form-data-get/1
  (lambda (k . default)
    (let ((vv (form-data-get k)))
      (cond ((pair? vv) (car vv))
            ((pair? default) (car default))
            (else "")))))

ここまでの例では、CGI スクリプトはプレーンテキストを生成していました。 しかし、一般には、HTML のページを生成してほしいですよね。 HTML フォームと CGI スクリプトの組み合せが、フォームをもつ一連の HTML のページのきっかけになることは珍しいことではありません。 これらのいろいろなフォームに関わるすべてのアクションを 一つの CGI スクリプトの中にコーディングすることもよくあることです。 いずれにせよ、HTML フォーマット、たとえば、適切にエンコードされた HTML の特殊文字などで文字列を書きだすユーティリティ手続き は役に立ちます。

(define display-html
  (lambda (s . o)
    (let ((o (if (null? o) (current-output-port)
                 (car o))))
      (let ((n (string-length s)))
        (let loop ((i 0))
          (unless (>= i n)
            (let ((c (string-ref s i)))
              (display
               (case c
                 ((#\<) "&lt;")
                 ((#\>) "&gt;")
                 ((#\") "&quot;")
                 ((#\&) "&amp;")
                 (else c)) o)
              (loop (+ i 1)))))))))

17.4  CGI による電卓

ここにあるのは CGI 電卓スクリプト、cgicalc.scm です。 これは Scheme の任意精度演算を使っています。

#!/bin/sh
":";exec /usr/local/bin/mzscheme -r $0

;CGI ユーティリティをロード
(load-relative "cgi.scm")

(define uhoh #f)

(define calc-eval
  (lambda (e)
    (if (pair? e)
        (apply (ensure-operator (car e))
               (map calc-eval (cdr e)))
        (ensure-number e))))

(define ensure-operator
  (lambda (e)
    (case e
      ((+) +)
      ((-) -)
      ((*) *)
      ((/) /)
      ((**) expt)
      (else (uhoh "unpermitted operator")))))

(define ensure-number
  (lambda (e)
    (if (number? e) e
        (uhoh "non-number"))))

(define print-form
  (lambda ()
    (display "<form action=\"")
    (display (getenv "SCRIPT_NAME"))
    (display "\">
  Enter arithmetic expression:<br>
  <input type=textarea name=arithexp><p>
  <input type=submit value=\"Evaluate\">
  <input type=reset value=\"Clear\">
</form>")))

(define print-page-begin
  (lambda ()
    (display "content-type: text/html

<html>
  <head>
    <title>A Scheme Calculator</title>
  </head>
  <body>")))

(define print-page-end
  (lambda ()
    (display "</body>
</html>")))

(parse-form-data)

(print-page-begin)

(let ((e (form-data-get "arithexp")))
  (unless (null? e)
    (let ((e1 (car e)))
      (display-html e1)
      (display "<p>
  =&gt;&nbsp;&nbsp;")
      (display-html
       (call/cc
        (lambda (k)
          (set! uhoh
                (lambda (s)
                  (k (string-append "Error: " s))))
          (number->string
           (calc-eval (read (open-input-string (car e))))))))
      (display "<p>"))))

(print-form)
(print-page-end)