前田修吾
私事で恐縮ですが、去る10月1日に初の子が産まれました。(電磁波の影響かど うかはわかりませんが女の子でした。) 出産にも立ち合ったのですが、女の人 というのはすごいものですね。以前、建築は男にとっての出産の代償行為であ る、というような話を物の本で読んだことがありますが、われわれプログラマ にとってはプログラミングがそれにあたるのかもしれません。いつかはぜひ Rubyのようなすばらしいプログラムを生み出したいものです。私の場合、その 前に一度水子供養をしないといけませんけれども。
さて、前回はCGIの代替としてのmod_rubyの利用について説明しましたが、 mod_rubyはたんなるCGIの代用品ではありません。今回は、CGIにはできないよ うなことを実現するためのmod_rubyの機能について説明します。
Apacheは非常に多くの機能を持っていますが、そのうちの大部分はモジュール と呼ばれるプラグインによって提供されています。たとえば、CGIやアクセス 制限などの機能も、モジュールによって提供されています。ユーザはApacheの コンパイル時に必要なモジュールだけをリンクしたり、Apache自体を再コンパ イルすることなく、動的に新しいモジュールをロードすることができます。こ のモジュールという仕組みによって、Apacheは非常に拡張性の高いWWWサーバ になっています。実は、mod_rubyもモジュールとして実装されており、Apache 本体のコードには一行の修正も加えられていません。(Rubyのコードには何度 か修正が加えられていますが。)
ただ、ApacheのモジュールはC言語で記述されるため、モジュールの作成には それなりの知識と技術が必要とされます。Apacheはメモリプールによってメモ リ管理を行っているため、メモリ管理の煩わしさ(つまりC言語を使用する上で の煩わしさの大半)からは解放されますが、それでもRubyによるお気楽なプロ グラミングにくらべると、かなり面倒な作業が必要とされます。
そこで、mod_rubyでは、RubyでApacheを拡張するための仕組みを提供していま す。今回は、RubyでApacheを拡張する方法について説明します。
Apacheはあるコンテンツがクライアントから要求された場合、たんにファイル の内容を返すだけでなく、ハンドラと呼ばれるものを呼び出し、何らかの処理 を行った結果を返すことができます。ハンドラはApacheモジュールによって提 供され、httpd.confでSetHandlerというディレクティブによってファイルが置 かれた場所に対して設定することができます。たとえば、List1は/cgi-binと いう場所に置かれたファイルをCGIスクリプトとして扱うための設定です。 cgi-scriptというのがCGIスクリプトを処理するハンドラの名前です。 cgi-scriptはmod_cgiというApacheモジュールによって提供されています。
mod_rubyではruby-objectというハンドラが用意されていますが、ruby-object ハンドラ自体は実際の処理を行いません。ruby-objectはRubyHandlerディレク ティブによって指定されたRubyオブジェクト(以下ハンドラオブジェクトと呼 びます)を呼び出し、実際の処理はそのオブジェクトが行います。したがって、 同じruby-objectというハンドラが設定されていたとしても、RubyHandlerで指 定されたハンドラオブジェクトが異なれば、行われる処理もまったく違ったも のになります。ハンドラオブジェクトの処理は当然ながらRubyで記述すること ができますから、ruby-objectハンドラを利用することでRubyでApacheを拡張 できるような仕組みになっているわけです。
List2は前回も紹介した、mod_rubyでRubyスクリプトを実行するための設定で す。RubyRequireはRubyのライブラリをロードするためのディレクティブで、 ここではapache/ruby-runというライブラリをロードしています。/rubyという 場所に対してSetHandlerディレクティブでruby-objectハンドラを設定し、 次のRubyHandlerディレクティブによって実際の処理を行うハンドラオブジェ クトを指定しています。RubyHandlerは引数にRubyの式を取り、式はリクエス トのたびに評価されます。この例ではApache::RubyRun.instanceというメソッ ド呼び出しを行っていますが、instanceはApache::RubyRunオブジェクトの唯 一のインスタンスを返すメソッドで、何度呼び出しても同じオブジェクトを返 します。RubyHandlerでFoo.newのような式を指定した場合には、リクエストを 処理するたびに、毎回インスタンスを生成することになります。
-- List1 CGI用のハンドラの設定 <Location /cgi-bin> SetHandler cgi-script Options +ExecCGI </Location> -- List2 Rubyスクリプト用のハンドラの設定 RubyRequire apache/ruby-run <Location /ruby> SetHandler ruby-object RubyHandler Apache::RubyRun.instance Options +ExecCGI </Location>
List3はApache::RubyRunを実装しているapache/ruby-runライブラリのソース コードですが、見ての通り、わずか25行で記述されています。このコードをも とに、RubyでApacheを拡張する方法を見て行きましょう。
まず、Apacheというモジュールの下にRubyRunクラスを定義していますが、 RubyRunクラスがハンドラオブジェクトのクラスになります。Apacheというモ ジュールはmod_ruby本体によって提供されるモジュールで、Apacheに関係する 機能を提供します。Apacheモジュールと呼ぶと、mod_cgiなどのApacheのモジュー ルと紛らわしいので、以下では::Apacheと呼ぶことにします。 *1
apache/ruby-run.rbでは::Apacheの下にクラスを定義していますが、必ずしも そうしなければならないわけではありません。ただ、::Apacheの外でモジュー ル定義をする場合は、::Apacheで定義された定数にアクセスするためには Apache::NOT_FOUNDのような形でアクセスしなければならないので注意が必要 です。(もちろん、クラスに::ApacheをインクルードすればNOT_FOUNDのような 形でアクセスできます。)
Apache::RubyRunはSingletonモジュールをインクルードしていますが、 Singletonは標準ライブラリのsingletonライブラリで提供されるモジュールで、 デザインパターンの一つであるSingletonパターンを実装します。
Singletonパターンはあるクラスのインスタンスがただ一つしか存在しないこ とを保証するパターンです。Singletonモジュールをインクルードしたクラス のnewはプライベートメソッドに変更され、外部からはアクセスできなくなり ます。インスタンスを得るためにはinstanceというクラスメソッドを利用しま す。instanceは最初の呼び出しでそのクラスの唯一のインスタンスを生成し、 それ以降の呼び出しではたんにそのインスタンスを返します。
一般にデザインパターンというのはライブラリとしてコードの再利用ができな いようなパターンを整理・分類することによって再利用できるようにしたもの ですが、Rubyの場合はその高い表現力と動的な性質からいくつかのパターンに ついてはライブラリとして提供されています。
Apache::RubyRunで定義されているメソッドはhandlerメソッドただ一つだけで す。RubyHandlerで指定されたオブジェクトは必ずこのメソッドを持たなけれ ばなりません。handlerメソッドはクライアントの要求に対し、実際に応答を 返すメソッドで、Apache::Requestオブジェクトを引数に取ります。 Apache::RequestはApache APIが提供するrequest_rec構造体のラッパで、クラ イアントからのリクエストを表現します。また、Apache::Requestはprintなど の出力系のメソッドを持ち、クライアントへの応答に利用されます。(実は mod_rubyでは$stdoutや$>もApache::Requestオブジェクトになっています。)
Apache::RubyRunのhandlerメソッドは、まずr.finfoによって要求されたファ イルの存在をチェックし、存在しない場合にはApache::NOT_FOUNDを返します。 r.finfoはFile::Statオブジェクトで、要求されたファイルの状態を表します。 ファイルが存在しない場合にはr.finfo.modeは0になります。
次にr.allow_optionsをチェックし、httpd.confなどでExecCGIオプション(CGI スクリプトの実行を許可するオプション)がオンになっているかどうかの確認 を行います。オンになっていない場合にはApache::FORBIDDENを返します。ま た、r.info.excutable?によってファイルの実行許可があるかどうかも確認し て、許可がない場合にはApache::FORBIDDENを返します。
これらのチェックを行った上で、いよいよRubyスクリプトを実行するための処 理を行います。まず、r.setup_cgi_envでCGI互換の環境変数の設定を行い、 Apache.chdir_file(r.filename)でカレントディレクトリをファイルが置かれ たディレクトリに変更します。そして、loadによってファイルをRubyスクリプ トとしてロードしています。クライアントへの応答はRubyスクリプトによって 行われます。最後にApache::OKを返して、ハンドラの処理が終了したことを表 しています。
handlerメソッドでは必ずハンドラの終了状態を表す値を返さなければならな いことに注意してください。返り値は数値ですが、Apacheモジュールに定数が 定義されているのでそれらを利用します。主な値をFig1に挙げておきます。
-- List3 apache/ruby-run.rb require "singleton" module Apache class RubyRun include Singleton def handler(r) if r.finfo.mode == 0 return NOT_FOUND end if r.allow_options & OPT_EXECCGI == 0 r.log_reason("Options ExecCGI is off in this directory", r.filename) return FORBIDDEN end unless r.finfo.executable? r.log_reason("file permissions deny server execution", r.filename) return FORBIDDEN end r.setup_cgi_env Apache.chdir_file(r.filename) load(r.filename, true) return OK end end end -- Fig1 ハンドラの主な返り値 OK 正常に処理が終了したことを表す DECLINED 処理を断わり、他のハンドラに任せる MOVED ステータスコード301(Moved Permanently)を返す REDIRECT ステータスコード302(Found)を返す AUTH_REQUIRED ステータスコード401(Unauthorized)を返す FORBIDDEN ステータスコード403(Forbidden)を返す NOT_FOUND ステータスコード404(Not Found)を返す SERVER_ERROR ステータスコード500(Internal Server Error)を返す
それでは、ここで簡単なハンドラを作ってみることにしましょう。
List4はContent-Typeのcharsetパラメータをファイルの内容から自動的に設定 するハンドラです。最近のApacheではcharsetが明示的に指定されていない場 合は勝手にcharsetにiso-8859-1が指定されますが、当然ながらそのままでは 日本語の文書はブラウザで正しく表示することができません。AddCharsetなど で明示的にcharsetを指定するのが正しいといえば正しいのですが、なかなか 面倒です。そこで、このハンドラではファイルの内容からNKFモジュールで文 字コードを推測し、自動的にcharsetを設定します。
Apache::AutoCharset#handlerでは、まずApache::RubyRun同様にファイルの存 在をチェックします。次にファイルの拡張子をチェックし、.htmlでも.htmで もない場合にはApache::DECLINEDを返します。Apache::DECLINEDを返した場合、 そのリクエストはデフォルトのハンドラによって処理されます。次にファイル をオープンして内容を読み込み、NKF.guessによって文字コードを推測し、 charsetを選択します。それから、Content-TypeとContent-Lengthを設定しへッ ダを出力した後、ファイルの内容を出力し、Apache::OKを返します。
List5はApache::AutoCharsetを利用するための設定例で、/ac以下のHTMLファ イルのcharsetを自動的に設定します。Apache::RubyRunと異なり、ExecCGIオ プションをオンにする必要はありません。
-- List4 apache/auto-charset.rb require "singleton" require "nkf" module Apache class AutoCharset include Singleton CHARSETS = Hash.new("iso-8859-1") CHARSETS[NKF::JIS] = "iso-2022-jp" CHARSETS[NKF::EUC] = "euc-jp" CHARSETS[NKF::SJIS] = "shift_jis" def handler(r) if r.finfo.mode == 0 return NOT_FOUND end if r.filename !~ /\.(html|htm)$/ return DECLINED end open(r.filename) do |f| body = f.read enc = NKF.guess(body) charset = CHARSETS[enc] r.content_type = format("text/html; charset=%s", charset) r.headers_out["Content-Length"] = body.length.to_s r.send_http_header r.print(body) return OK end end end end -- List5 Apache::AutoCharset用の設定 RubyRequire apache/auto-charset <Location /ac> SetHandler ruby-object RubyHandler Apache::AutoCharset.instance </Location>
Apacheモジュールでは、クライアントに実際に応答を返すハンドラ以外にも、 リクエストURIからファイルシステム上のファイル名への変換を行うハンドラ や、アクセス制御を行うハンドラなど、さまざまなハンドラを定義することが できます。
mod_rubyでもRubyXXXXHandler(XXXXの部分はハンドラの種類によって異なりま す)というディレクティブによって、さまざまな種類のハンドラを指定するこ とができます。以下では、代表的なディレクティブについて説明しますが、こ れらのディレクティブはRubyHandlerと違って、SetHandlerでruby-objectハン ドラを指定しておく必要はありません。SetHandlerはクライアントに実際に応 答を返すハンドラだけを指定するものなので注意してください。
RubyTransHandlerはリクエストURIからファイルシステム上のファイル名への 変換を行うハンドラを指定します。ApacheはURIからファイル名への変換を行 う時に、RubyTransHandlerで指定されたハンドラオブジェクトの translate_uri(r)というメソッドを呼び出します。(rはApache::Requestオブ ジェクト。) translate_uriはr.uriで得られるURIをファイル名に変換し、 r.filenameに設定しなければなりません。
RubyAuthenHandlerはユーザ認証を行うハンドラを指定します。ユーザ認証時 は、RubyAuthenHandlerで指定されたハンドラオブジェクトのauthenticate(r) というメソッドが呼び出されます。authenticateはクライアントから与えられ たユーザ名とパスワード(HTTP基本認証の場合は、r.get_basic_auth_pwでクラ イアントから与えられたパスワードが得られ、get_basic_auth_pwによって r.connection.userにユーザ名が設定されます)をチェックし、認証に成功した 場合はApache::OKを、失敗した場合はApache::AUTH_REQUIREDを返さなければ なりません。
RubyAuthzHandlerは、RubyAuthenHandlerで指定されたハンドラによって認証 されたユーザが、コンテンツにアクセスする権限を持っているかどうかをチェッ クするハンドラオブジェクトを指定します。チェック時には、ハンドラオブジェ クトのauthorize(r)というメソッドが呼び出されます。authorizeは r.connection.userで得られるユーザのアクセスを許可する場合には Apache::OKを、アクセスを許可しない場合にはApache::AUTH_REQUIREDを返さ なければなりません。
RubyAccessHandlerは、ユーザ認証以外の方法で(たとえばIPアドレスなど に基づく)アクセス制御を行うハンドラを指定します。アクセスのチェッ ク時にはcheck_access(r)というメソッドが呼ばれます。check_accessは アクセスを許可する場合にはApache::OKを、拒否する場合には Apache::FORBIDDENを返さなければなりません。
これら以外にも、以下のようなハンドラが利用可能です。
ファイルのMIMEタイプを設定するハンドラを指定します。
クライアントに応答を返す前に、その他の準備を行うハンドラを 指定します。
ログを記録するハンドラを指定します。
へッダのパーズを行うハンドラを指定します。
リクエストを読み込んだ後に実行されるハンドラを指定します。
各リクエスト毎の初期化を行うハンドラを指定します。
各リクエスト毎の後処理を行うハンドラを指定します。
List6はユーザ認証を行うハンドラの例です。ユーザ認証には RubyAuthenHandlerとRubyAuthzHandlerによって二つのハンドラを指定する必 要がありますが、これらのハンドラは呼び出されるメソッドの名前が異なるた め、一つのオブジェクトを両方のハンドラに設定することができます。
Apache::SillyAuthは名前の通りばかげた認証を行います。パスワードは生の ままでPASSWORDSというハッシュに登録されています。実用的なものを作成す る時にはファイルやデータベースなどを利用してください。
authenticateは、クライアントから与えられたパスワードをチェックして、パ スワードがPASSWORDSのものと一致すればApache::OKを、一致しなかった場合 にはApache::AUTH_REQUIREを返します。認証に失敗した場合には r.note_basic_auth_failureを呼んでいる点に注意してください。これを忘れ るとWWW-Authenticateへッダが出力されないため、ユーザにパスワードの入力 を求めることができません。
authorizeの方は、r.requiresでrequireディレクティブによって指定された条 件をチェックし、条件に一致する場合にはApache::OKを返し、一致しない場合 にはApache::AUTH_REQUIREDを返します。未知の条件であった場合には Apache::DECLINEDを返し処理を他のハンドラに任せます。r.requiresには配列 の配列が格納されています。各配列は二つの要素を持ち、一つ目の要素がメソッ ドのマスク値、二つ目の要素がrequireで指定された引数になっています。こ の例では手抜きをしてメソッドのマスク値の方のチェックは行っていません。
Apache::SillyAuth用の設定はList7のようになります。RubyAuthenHandlerと RubyAuthzHandlerの両方にApache::SillyAuthを指定しています。
-- List6 apache/silly-auth.rb require "singleton" module Apache class SillyAuth include Singleton PASSWORDS = { "guest" => "guest", "shugo" => "ruby" } def authenticate(r) pw = r.get_basic_auth_pw if pw == PASSWORDS[r.connection.user] return OK else r.note_basic_auth_failure return AUTH_REQUIRED end end def authorize(r) for method_mask, requirement in r.requires w, *args = requirement.split case w when "valid-user" return OK when "user" if args.include?(r.connection.user) return OK end else return DECLINED end end r.note_basic_auth_failure return AUTH_REQUIRED end end end -- List7 Apache::SillyAuth用の設定 RubyRequire apache/silly-auth <Location /auth> RubyAuthenHandler Apache::SillyAuth.instance RubyAuthzHandler Apache::SillyAuth.instance AuthType Basic AuthName "silly auth" require valid-user </Location>
さて、当初の私の心積りよりも随分長く続いたこの連載ですが、今回をもって 終了させていただくことになりました。連載の機会を与えてくださった後藤謙 太郎さんと編集の有馬さんには大変感謝しています。また、拙い文章をお読み いただいた読者のみなさんも本当にありがとうございました。
そうそう、この連載はこれで終りですが、Rubyの連載がなくなるわけではあり ませんので、次号以降もお楽しみに。私も締切がなくなったので次号を楽しみ にしています;-p それでは。
*1 ちなみに::ApacheというのはObject::Apacheの省略形で、実際にRubyプロ
グラムで利用できます。