(警告: 適切なセーフガードをほどこしていない CGI スクリプトはサイトのセキュリティを危うくします。ここにある スクリプトは単純な例であって、実際のウェブで利用を前提とする セキュリティを考えていません。)
CGI スクリプト [cite{cgi}] はウェブサーバに置かれたスクリプトで
クライアント(ブラウザ)から起動することができます。
クライアントは通常のページと全くおなじく、URL によって
CGI スクリプトにアクセスします。サーバはリクエストを受けた URL が
CGI スクリプトであることを認識し、それを走らせます。サーバが
どうやって、特定の URL をスクリプトとして認識するかはサーバの
管理者しだいです。このテキストではスクリプトは専用の cgi-bin
というディレクトリに置かれていると仮定しています。したがって、
サーバ www.foo.org
上のスクリプト testcgi.scm
は
http://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 への制御の受渡しは通常のスクリプトと同じです。
さて、Scheme CGI スクリプトの例です。testcgi.scm
は
CGI 環境変数で使われるもののいくつかの設定を出力するスクリプトです。
この情報は、新しく作られたページとしてブラウザに返されます。
返されるページは単に CGI スクリプトが標準出力に出したものです。
これが CGI スクリプトがそれを呼び出したところへ応える方法です。
これは新しいページを返すことでおこなわれます。
このスクリプトは最初に、
content-type: text/plain
という行を出力し、続いて空行を出力するということに
注意してください。これはウェブサーバがページをサービスする
標準的な儀礼です。この二つの行は実際にページとして表示されるものの
部分ではありません。ブラウザに送られてくるページが
(マークアップされていない)プレーンテキストであることを知らせるための
ものです。これによりブラウザは正しく表示ができます。テキストを
HTML でマークアップしたのなら、content-type
は text/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
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) form
が method=get
(デフォルト)であれば、情報は
QUERY_STRING
という環境変数を通じて送られます。(2) form
が
method=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}] を参照してください。
上の例ではパラメータの名前あるいは仮定されるその引数は、それ自身には
‘&
’ あるいは ‘=
’ の文字は含んでいませんでした。が、一般的には
含まれる可能性があります。そのような文字に対応し、セパレータと
間違うことのないように、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-string
は
QUERY_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 ((#\<) "<") ((#\>) ">") ((#\") """) ((#\&) "&") (else c)) o) (loop (+ i 1)))))))))
ここにあるのは 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> => ") (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)