第8回 mod_ruby(2)

前田修吾

近況報告

私事で恐縮ですが、去る10月1日に初の子が産まれました。(電磁波の影響かど うかはわかりませんが女の子でした。) 出産にも立ち合ったのですが、女の人 というのはすごいものですね。以前、建築は男にとっての出産の代償行為であ る、というような話を物の本で読んだことがありますが、われわれプログラマ にとってはプログラミングがそれにあたるのかもしれません。いつかはぜひ Rubyのようなすばらしいプログラムを生み出したいものです。私の場合、その 前に一度水子供養をしないといけませんけれども。

Apacheの拡張

さて、前回は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を拡張する方法について説明します。

ruby-objectハンドラ

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>

Apache::RubyRun

List3はApache::RubyRunを実装しているapache/ruby-runライブラリのソース コードですが、見ての通り、わずか25行で記述されています。このコードをも とに、RubyでApacheを拡張する方法を見て行きましょう。

::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のような 形でアクセスできます。)

Singletonモジュール

Apache::RubyRunはSingletonモジュールをインクルードしていますが、 Singletonは標準ライブラリのsingletonライブラリで提供されるモジュールで、 デザインパターンの一つであるSingletonパターンを実装します。

Singletonパターンはあるクラスのインスタンスがただ一つしか存在しないこ とを保証するパターンです。Singletonモジュールをインクルードしたクラス のnewはプライベートメソッドに変更され、外部からはアクセスできなくなり ます。インスタンスを得るためにはinstanceというクラスメソッドを利用しま す。instanceは最初の呼び出しでそのクラスの唯一のインスタンスを生成し、 それ以降の呼び出しではたんにそのインスタンスを返します。

一般にデザインパターンというのはライブラリとしてコードの再利用ができな いようなパターンを整理・分類することによって再利用できるようにしたもの ですが、Rubyの場合はその高い表現力と動的な性質からいくつかのパターンに ついてはライブラリとして提供されています。

handlerメソッド

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)を返す

Apache::AutoCharset

それでは、ここで簡単なハンドラを作ってみることにしましょう。

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

RubyTransHandlerはリクエストURIからファイルシステム上のファイル名への 変換を行うハンドラを指定します。ApacheはURIからファイル名への変換を行 う時に、RubyTransHandlerで指定されたハンドラオブジェクトの translate_uri(r)というメソッドを呼び出します。(rはApache::Requestオブ ジェクト。) translate_uriはr.uriで得られるURIをファイル名に変換し、 r.filenameに設定しなければなりません。

RubyAuthenHandler

RubyAuthenHandlerはユーザ認証を行うハンドラを指定します。ユーザ認証時 は、RubyAuthenHandlerで指定されたハンドラオブジェクトのauthenticate(r) というメソッドが呼び出されます。authenticateはクライアントから与えられ たユーザ名とパスワード(HTTP基本認証の場合は、r.get_basic_auth_pwでクラ イアントから与えられたパスワードが得られ、get_basic_auth_pwによって r.connection.userにユーザ名が設定されます)をチェックし、認証に成功した 場合はApache::OKを、失敗した場合はApache::AUTH_REQUIREDを返さなければ なりません。

RubyAuthzHandler

RubyAuthzHandlerは、RubyAuthenHandlerで指定されたハンドラによって認証 されたユーザが、コンテンツにアクセスする権限を持っているかどうかをチェッ クするハンドラオブジェクトを指定します。チェック時には、ハンドラオブジェ クトのauthorize(r)というメソッドが呼び出されます。authorizeは r.connection.userで得られるユーザのアクセスを許可する場合には Apache::OKを、アクセスを許可しない場合にはApache::AUTH_REQUIREDを返さ なければなりません。

RubyAccessHandler

RubyAccessHandlerは、ユーザ認証以外の方法で(たとえばIPアドレスなど に基づく)アクセス制御を行うハンドラを指定します。アクセスのチェッ ク時にはcheck_access(r)というメソッドが呼ばれます。check_accessは アクセスを許可する場合にはApache::OKを、拒否する場合には Apache::FORBIDDENを返さなければなりません。

これら以外にも、以下のようなハンドラが利用可能です。

RubyTypeHandler

ファイルのMIMEタイプを設定するハンドラを指定します。

RubyFixupHandler

クライアントに応答を返す前に、その他の準備を行うハンドラを 指定します。

RubyLogHandler

ログを記録するハンドラを指定します。

RubyHeaderParserHandler

へッダのパーズを行うハンドラを指定します。

RubyPostReadRequestHandler

リクエストを読み込んだ後に実行されるハンドラを指定します。

RubyInitHandler

各リクエスト毎の初期化を行うハンドラを指定します。

RubyCleanupHandler

各リクエスト毎の後処理を行うハンドラを指定します。

Apache::SillyAuth

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プロ グラムで利用できます。