![]() |
|
1. EusLisp for UNIX script programming
通常、Lispを用いるときは、キーボードから式を与えてその評価結果を受け取るインタラクティブなサイクルを繰り返しますが、CGIのプログラムは、そのようなインタラクティブな動作ではなく、一発芸で実行されるようなunixのコマンド形式にプログラムされる必要があります。EusLispは、起動時の引数をプログラムファイル名だと思ってロード(実行)します。例として、current directoryにあるファイルの個数を出力するプログラムを考えます。
% cat filecount.l
(print (length (directory)))
% cat filecount
#! /bin/csh
setenv EUSDIR /usr/local/eus
setenv LD_LIBRARY_PATH /usr/local/eus/Linux/lib
/usr/local/bin/eus filecount.l
% chmod a+x filecount
ここで、filecount を実行すると、ファイルの個数が表示されます。二つあるsetenvは、.cshrcの中に定義されていれば不要です。そのときは、#! /bin/cshの代わりに#! /usr/local/bin/eus とすることも可能です。
% cat filecount
#! /usr/local/bin/eus
(print (length (directory)))
% ./filecount </dev/null
euslispは、標準入力がttyの場合は、初期化されるモジュールを表示します。これをなくすには、/dev/nullを標準入力に指定するか、ソースプログラムが標準入力から読み込まれるようにします。
% cat filecount2
#! /bin/csh
setenv EUSDIR /usr/local/eus
setenv LD_LIBRARY_PATH /usr/local/eus/Linux/lib
/usr/local/bin/eus <<EOF
(print (length (directory)))
EOF
ただし、このようにして標準入力にプログラムを与える方法は、CGIスクリプトの記述には不適当です。CGIでは、クライアントからのリクエストがPOSTされると、引数が標準入力から流し込まれるからです。
さらに、表示をxwindowに出すのであれば、次のようにします。
% cat filecount
#! /bin/csh
setenv EUSDIR /usr/local/eus
setenv LD_LIBRARY_PATH /usr/local/eus/Linux/lib
/usr/local/bin/eusx <<EOF
(setq xw (instance x:textwindow :create))
(send xw :put-line 1 1 (format nil "filecount=‾d‾%" (length (directory))))
(xflush)
(unix:sleep 5)
EOF
引数のあるコマンドしたい場合、$1, $2 などのシェル変数で受け取ります。次の例は、引数に与えられた文字列をローマ字からひらがなに変換するものです。
% cat kana
#! /bin/csh
/usr/local/bin/eus <<EOF
(load "lib/llib/kana_euc.l")
(print (romkan "$1"))
EOF
% chmod a+x kana
% ./kana matsui
"まつい"
2. EusLispからPostgreSQLを使う
PostgreSQLは、Linuxを含むUNIX上でフリーで利用できる、
リレーショナルデータベースです。
研究目的なので、型の定義やクラスの継承など、
意欲的な機能が取り込まれています。
フリーなだけに文句は言えませんが、
oid (object ID) の回収(GC)が完全には行われないと言う、
Lisp派から見ると容認しがたい問題もあります。
(GCで悩んだりSQLなんていうへんてこりんな言語の解釈にわずらわされる
くらいならLispで実装すればよいのに。)
ここではあまり気にせず、まず、postgres (pgsql) をインストールします。
インストールの方法は、
http://www2.gol.com/users/tosiyuki/linux/index.html などを参考にして下さい。
以下では、version 6.5.3 を想定しています。適当なユーザー名をcreateuserで定義し、適当なデータベースをcreatedbで作成して下さい。ここでは、ログイン名でcreateuser, createdbしてあるものと仮定します。また、postmaster -i & などのコマンドで、postgreSQLサーバーをバックグラウンドで走らせておきます。正しく動作することをpsqlコマンドで確認します。
EusLispは、postgresとインタフェースするために、
libpq.so というライブラリを使用します。
libpq.soは、postgresが正しくインストールされれば、
/usr/local/pgsql/lib/libpq.soとして作成されているはずです。
これをそのまま用いても良いですし、
/usr/local/libなどにコピーしても良いでしょう。
libpq.soをリンクし、外部関数として定義し、
データベースとの接続を管理するクラスを作成するeuslispのプログラムが、
/usr/local/eus/lib/llib/pgsql.l にあります。
libpq.soの場所は、pgsql.lの中に書かれていますので、
ライブラリの参照は、必要ならば書き換えます。
pgsql.lは、"PQ"というパッケージを作成し、
種々のオブジェクトをその中に定義します。
pgsql.lがロードされると、次のようにしてpostgreSQLサーバと接続します。
(setq db (instance pq:pgsql :init))データベースやユーザーの名前がデフォルト(ログイン名)と異なる場合や、postgreSQLサーバが別のマシンで走っているときは、次のようにして指定します。
(setq db (instance pq:pgsql :init :dbname "utyo" :user "skagami" :host "jsk.t.u-tokyo.ac.jp"))
table_policyの主要なフィールドには、tablename, screen, fields, links, titleがあります。tablenameは、文字通り、動作制御の対象となるテーブルの名前を指定します。screenは、表示画面の種類です。データベースとwebの組み合わせには、アプリケーションに応じて無限の利用法がありますが、主な基本操作は、一覧表の表示、詳細表示、検索、レコードの追加と削除、データの更新、などになります。それらに対応して、list, detail, search, insert, changeなどのscreenを定義します。さらに、テーブルの付加情報を定義するために、仮想的にtopというscreenを用意しています。fields には、そのscreenで表示するフィールド名をかっこで囲まれたリストで表現します。linksには、ボタンでリンクする画面や制御を指定します。例として、list screenのlinksに、(search insert list-next list-previous) というリストを指定すると、探索画面、 新規レコード作成画面、次の一覧表、前の一覧表、にリンクするボタンが表示されます。mailを指定すると、選択されたレコードにemailフィールドがあれば、それらにemailを送信する画面になります。 % cat eusdb.fcgi
6. table_policyによるデータベースアクセスと表示の制御
table_policyは、PostgreSQLデータベースのテーブルをhttpdb.lを使って外部に公開するときの画面を制御します。次のようなsqlコマンドをpsqlの¥iコマンドで読み込ませることによりとりあえずの初期データを作ることができます。
drop table table_policy;
drop sequence table_policy_id_seq;
create table table_policy (
id serial primary key,
password text,
tablename text,
screen text,
auth text,
fields text,
links text,
title text,
description text);
-- records in the table_policy
insert into table_policy
(tablename, screen, fields, links, title)
values
('table_policy',
'top',
'(id)',
'(("Search" search) ("List" list))',
'table_policy top');
insert into table_policy
(tablename, screen, fields, links, title)
values
('table_policy',
'list',
'(id tablename screen fields links title)',
'(("Search" search) ("List" list) ("insert" insert) list-next list-previous)',
'table_policy list');
insert into table_policy
(tablename, screen, fields, links, title)
values
('table_policy',
'search',
'(tablename screen fields links title)',
'(("List" list))',
'table_policy search');
insert into table_policy
(tablename, screen, fields, links, title)
values
('table_policy',
'insert',
'(id tablename screen fields links)',
'(("List" list))',
'table_policy insert');
insert into table_policy
(tablename, screen, fields, links, title)
values
('table_policy',
'detail',
'(id tablename screen fields links title description)',
'(("List" list) change ("Delete" delete) change-password)',
'table_policy detail');
insert into table_policy
(tablename, screen, fields, links, title)
values
('table_policy',
'change',
'(tablename screen fields links title description)',
'(("List" list) ("Delete" delete))',
'update a table_policy record');
---- weather_report
insert into table_policy
(tablename, screen, fields, links, title)
values
('weather_report',
'top',
'(id)',
'(search list)',
'Weather_Report');
insert into table_policy
(tablename, screen, fields, links, title)
values
('weather_report',
'detail',
'(id district time abstract today tomorrow max_temp min_temp)',
'(("search" search) ("Delete" delete))',
'Detail of weather_report');
insert into table_policy
(tablename, screen, fields, links, title)
values
('weather_report',
'search',
'(id district time max_temp min_temp)',
'(("List" list))',
'Search in weather_report');
insert into table_policy
(tablename, screen, fields, links, title)
values
('weather_report',
'list',
'(id district abstract min_temp max_temp)',
'(("Search" search) ("Insert" insert))',
'list of weather_report');
-- people
insert into table_policy
(tablename, screen, fields, links, title)
values
('people',
'top',
'(id)',
'(("Search" search) ("List" list) ("Insert" insert))',
'People table');
insert into table_policy
(tablename, screen, fields, links, title)
values
('people',
'list',
'(id account kanji_lastname kanji_firstname email )',
'( search insert download )',
'list of people');
insert into table_policy
(tablename, screen, fields, links, title)
values
('people',
'detail',
'(id account kanji_lastname kanji_firstname romanji_firstname
romanji_lastname email tel_number secretary office)',
'(change delete search list)',
'Detail of a people record');
insert into table_policy
(tablename, screen, fields, title)
values
('people',
'search',
'(id account kanji_lastname kanji_firstname romanji_firstname
romanji_lastname email tel_number secretary office)',
'Search in the people table');
insert into table_policy
(tablename, screen, fields, title)
values
('people',
'change',
'(id account kanji_lastname kanji_firstname romanji_firstname
romanji_lastname email tel_number secretary office)',
'Search in the people table');
insert into table_policy
(tablename, screen, fields, title)
values
('people',
'insert',
'(id account kanji_lastname kanji_firstname romanji_firstname
romanji_lastname email tel_number secretary office)',
'Insert a new people record');
list screen (一覧表示)では、レコードの先頭にあるidを選択すると、detail 画面に移行し、そのレコードの詳細が表示されます。detail screenのlinksに、delete, changeがあれば、各々、レコード削除、レコード更新の画面に移行します。一覧画面で、フィールド名をクリックすると、そのフィールドの内容でソートされます。テーブルの中には、idフィールドがなければなりません。idフィールドは、その値を決定すると、どのレコードかがユニークに制約されるものでなければなりません。たとえば、シリアル番号を与えるために、serial 型を宣言したフィールドなどが適当です。どのフィールドをidとするかは、top screenのfields リストの最初の要素で指定します。また、top screenのfieldsリストの二つ目の要素は、更新時間を記録するフィールドを指定します。idフィールドは、一覧表では必ず表示されるようにlist screenのfieldsに含めておく必要があります。さもないと、そのレコードの詳細情報を表示できません。
これらの情報をうまくtable_policyに記述しておくと、それだけで、各種のテーブルをよく似た方法で閲覧・更新・管理することが可能になります。table_policyは、ブラウザに表示される画面の動作を記述するので、一種のtable駆動型のプログラムといえます。table_pollicyもテーブルの一種であり、このテーブルを操作する方法がtable_policy自体の中に書かれている、というのは一種のreflectionともいえます。
そのときのURIはいずれも、http://www.site.jp/cgi-bin/eusdb.cgiであり、例のデータベースへのログイン画面からたどって行ってもよいのですが、普通の応用では、特定のデータベースの特定のテーブルに直行したいものです。どのテーブルのどの画面を見せるかは、?以下の引数で指定します。http://www.site.jp/cgi-bin/eusdb.cgi?database=matsui&user=matsui&port=5432&table=people&command=list は、peopleテーブルの一覧表を見せるURLとなります。
7. Fast-CGI
Apache web serverには、Fast-CGIという、CGIの起動効率を改善する仕組みを組み込むことができます。すなわち、通常のCGIが、リクエストの度にhttpdがCGI処理プロセスをforkして引数などを標準入力から送り、結果のhtmlドキュメントを標準出力から受け取るするのに対し、Fast-CGIは、Fast-CGI処理プロセスをずっと生きたままに維持し、ソケットで通信することでプロセスの起動にかかる時間を節約します。何もしないで終わるperlの起動には、400MHzのPentium-IIのLinux2.2で約0.03秒、eusの起動には約0.3秒かかります。pgsql.lをロードし、postgreSQLデータベースと接続すると、一回の起動に必要な最小の時間は0.45秒となります。euslispでCGIを書くと、毎秒1回以上の速度のリクエストには支障を来すことになります。さらに、一つのpostgreSQLテーブルを連続して操作する場合、それまでの操作の履歴を使いたくなりますが、CGIでは、毎回接続が切れますので、状態を維持できません。したがって、URIとして長々とした引数を毎回送る必要が生じます。これは、効率が悪いだけでなく、パスワードのような秘匿情報をたびたび送ることになり、セキュリティ上も問題となります。fast-CGIは、これらの問題を一挙に解決します(ただし、デバッグは多少面倒になります)。servletも似た機能を提供しますが、独立したプロセスになっていて、記述言語を選ばない分、fast-CGIの方が汎用的でしょう。
fast-cgiを利用するためには、まず、apacheに、mod_fastcgiをロードしておく必要があります。mod_fastcgiは、www.fastcgi.orgから入手できます。httpd.confにLoadModule fastcgi_module /usr/libexec/mod_fcgi.so, AddModule mod_fastcgi.c の2行を追加します。(LoadModuleは、shared objectの名前、AddModuleにはソースファイルの名前を指定するというのは、いかにもパッチワークですね。)また、srm.confには、fcgiのプログラムが置かれた場所とそのプログラムの名前を指定します。つまり、<Location /fcgi> SetHandler fastcgi-script </Location>を3行で、またAppClass /home/httpd/html/fcgi/eusdb.fcgi -port 9000のような行を定義します。
euslisp fcgiプログラムは、libfcgi.soをリンクします。libfcgi.soは、www.fastcgi.orgからダウンロードできるfcgi-devkit-2.1から作成します。普通にmake すると、libfcgi.aができてしまうので、shared objectが生成されるようにMakefileを作り替える必要があります。
EusLispでfcgi プログラムを記述する場合は、冒頭でlib/llib/httpfcgi.lをロードするようにします。このプログラムは、libfcgi.soをリンクして標準入出力をソケットに置き換え、apacheからの接続を待ちます。そのときのポート番号は、srm.confで指定した番号になります。接続が確立した後ですることは、通常のCGIプログラムとだいたい同じです。htmlヘッダを出力し、データベースを操作するのであれば、httpdbを呼び出します。このようなサイクルは、fcgi-loopというマクロで簡単に書けるので、次のようなソースプログラムを用意して置いて、httpd/html/fcgi/eusdb.fcgiなどのスクリプトから呼び出します。
#! /bin/csh
setenv EUSDIR /usr/local/eus ; setenv LD_LIBRARY_PATH /usr/local/eus/Linux/lib
/usr/local/bin/eus /usr/local/eus/lib/demo/httpdb-fcgi.l
% cat /usr/local/eus/lib/demo/httpdb-fcgi.l
(load "/usr/local/eus/lib/llib/httpfcgi.l")
(http-loop (html-header) (gen "‾%‾%") (httpdb))
8. cookieによる接続の維持
先に述べたように、起動の高速性に加えて、fcgiを使う利点は、接続のステータスを保持できる点にあります。接続ステータスは、euslispでは、httpfcgi.lに定義された、fcgi-connectionというオブジェクトに保持されます。特定のweb browserからのリクエストが、どの接続に対応するかを識別するために、cookieを用います。cookieは、CGIあるいはFCGIプログラムからのHTTPヘッダの中でSet-Cookieディレクティブによってクライアントに伝えられ、web browserのメモリの中、あるいはクライアントPC上の特定のファイルの中に保持されます。cookieは、keyとvalueのペアを定義します。また、そのcookieが有効な期限、ドメインを定義します。CGIあるいはFCGIプログラムは、(html-header)を送出し、改行を連続して二つ送る前に(http ヘッダの中で)、set-cookie関数によってcookieの保存をブラウザに要請します。ブラウザは、URLを参照するたびにcookieに指定された有効期間と有効ドメインを検査し、有効なcookieをすでに蓄積して持っていれば、それをweb serverに送り出します。たとえば、www.eus.go.jpというweb serverが、connection-idをkeiとし、12345をvalueとしたcookieを保存しているとします。ユーザーがこのサーバーをホストフィールドに含むURLを参照し、cookieの有効期限がexpireしていなければ、このcookieをweb サーバーに送り返します。
web serverがクライアントからのcookieを読み込むと、それをHTTP_COOKIEという環境変数にセットして、CGIあるいはFCGIプログラムを呼び出します。EusLisp FCGI スクリプトは、接続を受け付けた直後にget-cookie関数を呼び出すことによって、*cookies*に、((key1 value1) (key2 value2) ...) というcookieのリストを作成します。同じweb serverの中の別のFCGIプログラムが送ったcookieと混同しないよう、アプリケーション (FCGIプログラム)は、互いに異なるkeyにvalueを対応づけなければなりません。
こうして、cookieが得られると、これを識別子にして*fcgi-connections*リストを走査して、指定されたkeyとvalueのペアを持つfcgi-connectionオブジェクトを探します。その際、cookieを送り出してからあまりに長時間が経過した接続オブジェクトがあれば、それを消去します。現在の実装では、cookieは、fcgiプロセスのメモリ中にだけ保持され、fcgiプロセスがエラーなどでリスタートすると、以前の接続ステータスは失われます。fcgi-connectionの中に維持される情報は、PostgreSQLデータベースとの接続(pq:pgsqlオブジェクト)とパスワードの認証履歴です。
9. パスワードと認証
Webから互いに見ず知らずの多数の人がCGIを経由してPostgreSQLのテーブルを操作すると、テーブルやレコードを不正なアクセスから保護する機構が必要になります。WELDでは、テーブル単位とレコード単位の2種類のプロテクションを行います。テーブルへのレコードの追加と削除には、そのテーブルに対するパスワード認証を必要とします。あるレコードの内容を書き換えるには、そのレコード固有のアクセスパスワードが必要です。前者をスクリーンパスワード、後者をレコードパスワードと呼びます。スクリーンパスワードは、table_policyの、delete および insert screenが記録されているレコードのpasswordフィールドに格納します。レコードパスワードは、保護したいテーブルのpasswordというフィールドに保存されます。つまり、レコードごとにパスワードでの保護をかけたいのなら、そのテーブルにはtext型の'password' フィールドが定義されていなければなりません。いずれのパスワードも、データベース中では暗号化されたテキストとして保存されています。したがって、忘れてしまったパスワードを思い出すことはできません。
新しいレコードを追加するときは、そのテーブルのdelete screenのパスワードに指定されたパスワードを入力します。レコードの内容を変更するときは、そのレコードのレコードパスワードの入力を求められます。レコードにパスワードが指定されていないときは、テーブルのchange screenのパスワードの入力を求めます。同様に、レコードを削除するときも、レコードパスワード、スクリーンパスワードの順に入力を求めます。
ここでで述べたパスワード認証は、FCGI/EusLispが独自に行う認証方法であって、PostgreSQLがデータベース接続をパスワードで保護するのとは異なります。参考までに、PostgreSQLのパスワード認証の方法を記しておきます。
PostgreSQLのアクセス制御は、データベースとホスト計算機とユーザーの組み合わせで行うものと、テーブルとユーザーの組み合わせで行うものの2種類があります。前者は、$PGSQL/data/pg_hba.confファイルで、後者は、grant/revokeなどのSQLコマンドで設定します。
ホスト計算機は、PostgreSQLサーバーが走っているそのマシンと、ネットワーク接続される他のマシンに分けられます。実際は、前者はunixドメインのソケットで接続され、後者はInternetドメインのソケットで接続されるという違いなので、PostgreSQLと同一マシンで走っていても、IP接続するものは後者と同様に扱われます。ホストコンピュータは、IPアドレスにマスクをかけることで、あるLANに接続されたホストのグループとして識別します。そのようなホストのいずれかが、あるデータベース、あるいはすべてのデータベースに接続しようとしたとき、そのマシンを信頼してアクセスを許すか (trust)、拒絶するか (reject)、それともパスワードでユーザーごとの認証をするか(password)、を選択します。passwordを選択するときは、pg_passwordプログラムによって、ユーザー名と暗号化されたパスワードを関連づけるファイルを作成し、それを$PGDATAディレクトリに置きます。
データベースjに接続した後、その中の特定のテーブルを見たり変更を加えたりできるかは、grant コマンドで設定します。grantを得たユーザーは、パスワードを入力することなくテーブルにアクセスできます。したがって、特定のユーザーにだけgrantを出したとしても、そのユーザーがホスト-データベース-パスワードの認証を経ずにデータベースに接続できる状態になっているとすると、そのユーザーの名前を知っている者は誰でもgrantを得ているのに等しくなります。したがって、grantを得たユーザーは、上記のようにそのデータベースに接続するときはパスワードの認証を経るようにし、pg_passwordによってパスワードを設定しておかなければ意味がありません。PostgreSQLでできるのは、テーブル単位でのアクセス制御までで、レコードごとの制御はできません。