第7回 mod_ruby(1)

前田修吾

引数の操作

先日、cgiライブラリを使っていて、ちょっとしたバグを見つけました。 *1 cgiライブラリはRubyでCGIプログラムを書く時に使われるライブラリで、標準 添付ライブラリに含まれています。cgiライブラリはフォームによる入力デー タのデコードや、Content-Typeなどのへッダの出力機能を持っているのですが、 私がバグを発見したのはへッダの出力機能でした。

cgiライブラリを使ってへッダの出力を行うためには、CGI#headerというメソッ ドを利用します。CGI#headerはHashによってオプションを指定することによっ て、へッダの出力をコントロールします。たとえば、List1のようなスクリプ トでは、Content-Typeにtext/plainを指定し、Content-Typeのcharsetパラメー タにiso-8859-1を指定しています。(実行結果はFig1)

問題が起きたのは、List2のようなスクリプトです。List2ではTEXT_PLAINとい う定数を定義して、CGI#headerの引数に利用しています。また、ボディの部分 でもTEXT_PLAINの値を出力しています。このスクリプトを実行すると、奇妙な ことにTEXT_PLAINの値が変更されて、元の値("text/plain")に余分な文字列 ("; charset=iso-8859-1")が追加されてしまいます。(Fig2)

いったいこれはどうしてでしょうか。実際に問題を発見したのはもっと複雑な プログラムだったため、最初は自分のプログラムを疑いました。(私は短期間 に数多くのバグを見つけ出す優秀なプログラマですが、その成果の大半は自分 自身が書いたコードから得られます。) しかし、注意深くソースを読み返して みても、どうしても原因を見つけることができませんでした。

そこで、CGI#headerの処理が怪しいと思った私は、cgiライブラリのソースを 読んでみることにしました。(こういう時にすぐにソースを見ることができる というのは、非常にありがたいことです。)

原因はCGI#headerの引数の処理にありました。CGI#headerはoptionsという引数 によってオプションをHashで受け取るのですが、optionsに対してList3のよう な処理を行います。ここで、String#concatというメソッドが利用されていま す。String#concatは文字列の連結を行うメソッドです。String#+との違いは、 レシーバの文字列そのものを変更してしまうことです。たとえば、

a = "abc"
b = a + "def"

ではaは"abc"のままですが、

a = "abc"
a.concat("def")

ではaそのものが"abcdef"になってしまいます。この時、変数aが指すオブジェ クトがconcatの前と後で変わっているわけではありません。変数aはconcatの 後も同じ文字列オブジェクトを指しているのですが、concatによってその文字 列オブジェクトそのもの内容が"abc"から"abcdef"に変更されてしまうのです。

CGI#headerの問題は、引数で渡された文字列に対してString#concatを適用し てしまっていたことにあります。String#concatによって文字列オブジェクト そのものが変更されてしまうため、メソッドの呼び出し側の方に予期せぬ 副作用が起こってしまっていたのです。この問題は、List4のように、文字列 オブジェクトそのものの内容を変更する代りに、options["type"]に+によって 連結した文字列をセットすることによって解決することができます。 *2

String#concatのように、オブジェクトの状態を変えてしまうメソッドのこと を、破壊的(destructive)なメソッドと呼びます。あるメソッドの引数で与え られたオブジェクトに、破壊的なメソッドを適用した場合、オブジェクトの状 態が変わってしまうため、そのメソッドの呼び出し側にも影響が出ることにな ります。したがって、とくにそうする必要がないかぎり、引数には破壊的なメ ソッドを適用するべきではありません。

また、Stringのように変更可能なオブジェクトのことを、可変(mutable)であ るといいます。反対に、Fixnumのように変更不可能なオブジェクトのことを 不変(immutable)であるといいます。不変なオブジェクトはそもそも内容を変 更するような操作を行うことができません(違う値が必要な場合には、そのオ ブジェクトの値を変更する代りに、その値を持つ他のオブジェクトを利用しま す)から、引数で与えられた場合にも、可変なオブジェクトのように気を使う 必要がありません。Javaなどのいくつかの言語では文字列は不変なオブジェク トですが、RubyのStringは可変なので、文字列操作が手軽な反面、扱いには注 意が必要です。

-- List1 cgiライブラリの利用

  require "cgi"

  cgi = CGI.new
  print cgi.header("type" => "text/plain",
                   "charset" => "iso-8859-1")
  print "hello world\n"

-- Fig1 List1の実行結果

  Content-Type: text/plain; charset=iso-8859-1

  hello world

-- List2 バグが再現するスクリプト

  require "cgi"

  TEXT_PLAIN = "text/plain"

  cgi = CGI.new
  print cgi.header("type" => TEXT_PLAIN,
                   "charset" => "iso-8859-1")
  printf("TEXT_PLAIN: %s\n", TEXT_PLAIN)

-- Fig2 List2の実行結果

  Content-Type: text/plain; charset=iso-8859-1

  TEXT_PLAIN: text/plain; charset=iso-8859-1

-- List3 CGI#headerの一部

  if options.has_key?("charset")
    options["type"].concat( "; charset=" )
    options["type"].concat( options.delete("charset") )
  end

-- List4 CGI#headerの修正

  if options.has_key?("charset")
    options["type"] = options["type"] +
      "; charset=" + options.delete("charset")
  end

mod_rubyとは

さて、今回はmod_ruby[1]についてお話しします。mod_rubyとは、 ApacheというHTTPサーバ上で動作する、Webアプリケーションの実行環境です。

Webアプリケーションには大きく分けて二種類ありますが、mod_rubyがサポー トするのはサーバサイドアプリケーションです。サーバサイドアプリケーショ ンというのは、サーバ側で動作するアプリケーションのことです。(そのまま ですね。) 一方、Javaアプレットのようにクライアント側で動作するアプリケー ションは、クライアントサイドアプリケーションと呼ばれ、サーバサイドアプ リケーションと区別されます。

サーバサイドアプリケーションといえば、もっともポピュラーなのは CGI[2]ですが、CGIではリクエスト毎にCGIプログラムを実行するための プロセスが生成されるため、あまり効率的ではありません。CGIプログラムに Rubyスクリプトを利用する場合はRubyスクリプト自体の処理が遅いので、プロ セス生成のコストくらいは問題にならないのではないかと思われる方もいるか もしれません。しかし、プロセスの生成は非常に重い処理なので、Rubyスクリ プト自体の遅さを考慮しても、かなりの影響があります。

そこで、mod_rubyはApache自体にRubyインタプリタを組み込むことによって、 新たにプロセスを生成することなくRubyスクリプトを実行します。それによっ て、mod_ruby上で動作するアプリケーションはCGIに比べて高速に動作します。

mod_rubyを使ってはいけない理由

mod_rubyは有用なアプリケーションですが、万能ではありません。したがって、 mod_rubyを利用するのが適切でないような局面では、有用でないどころか、有 害ですらあり得ます。ここでは、mod_rubyを利用するべきでないケースについ て簡単に説明しておきます。

mod_rubyの利用でよく問題になるのは、複数のスクリプトが一つのインタプリ タを使い回すという点です。現在のRubyインタプリタは複数の環境を切り替え る機能がないため、あるスクリプトがグローバルな状態を変更してしまうと、 その変更の影響が他のスクリプトにも及ぶことになります。たとえば、Rubyで は$,というグローバル変数に文字列を指定すると、その文字列がprintの各引 数の間に出力されますが、あるスクリプトが$,に値を設定すると、その値が他 のスクリプトにも影響することになります。そのため、mod_ruby上で動作させ るスクリプトを書く時は、グローバルな状態を変更しないように注意する必要 があります。もっとも、まともなRubyプログラマであれば、わざわざ注意しな くても普段からそのようにプログラムを書いているでしょうから、それほど心 配することはありません。また、このことには良い面もあります。たとえば、 Rubyでは一度requireしたライブラリは、二度目以降にrequireしても実際には ロードされません。したがって、スクリプトな実行する度に何度も巨大なライ ブラリをロードするという無駄をなくすことができます。

一方でこの問題が致命的になるケースもあります。今まではmod_ruby用のスク リプトを書くプログラマに悪意がないことを前提にして来ましたが、もしも 悪意のあるユーザがmod_ruby上でスクリプトを動作させる権限を持っていたと したらどうでしょう。そのユーザは組み込みのメソッドを再定義したりするこ とによって、簡単に他のユーザのスクリプトの挙動を変えることができてしま います。mod_rubyはなるべくグローバルな状態がスクリプトによって変更され ることがないように普通にトップレベルでメソッドを定義してもObjectにメソッ ドが定義されないようになっていますが、これはあくまでもユーザのうっかり ミスを避けるための処置で、セキュリティを実現するためのものではありませ ん。したがって、信頼できないユーザには、mod_ruby上でスクリプトを動作さ せる権限を与えるべきではありません。そのような状況では、CGIなどの他の 手段を利用した方がよいでしょう。

CGI互換環境

mod_ruby以外にもCGIの実行効率の改善を行うためのアプリケーションはいく つかあります。FastCGIもその一つです。CGIの場合は、リクエストのたびに CGIプログラムを実行するプロセスを生成しますが、FastCGIの場合はCGIプロ グラムに相当する処理を行うサーバプログラムを走らせておいて、HTTPサーバ は新たにプロセスを生成する代りにそのサーバに接続してやり取りを行います。 通信のコストは余分にかかりますが、プロセスの生成のコストに比べればたい したことはありませんから、FastCGIはその名前の通り非常に効率良く動作し ます。ただ、FastCGIには一つ問題があります。それは、CGIとはまったく異な るインタフェイスを持つため、CGIからの移行に手間がかかるということです。

そこで、mod_rubyはNPH CGI互換のインタフェイスを提供することによって、 CGIからの移行を容易にしています。NPH CGIというのは、HTTPサーバはまった くHTTPへッダの出力を行わず、スクリプト自身がステータスラインを含むすべ てのへッダを出力する必要のあるタイプのCGIです。このように通常のCGIとは 若干動作が異なるのですが、この違いはcgiライブラリによって吸収すること ができます。また、へッダの扱い以外については通常のCGIと変わりありませ ん。たとえば、フォームからGETメソッドで送信されたデータは環境変数 QUERY_STRINGで取得することができますし、POSTメソッドで送信されたデータ は標準入力によって取得することができます。

List5はCGIとmod_rubyを利用するためのApacheの設定ファイル(httpd.conf)の 設定例です。この例では、/usr/local/apache/cgi-binというディレクトリ下 のファイルに対して、/cgi-bin/filenameというURIでアクセスした場合はCGI スクリプトとして実行し、/ruby/filenameというURIでアクセスした場合には mod_rubyスクリプトとして実行する、という設定を行っています。

たとえば、List6のようなスクリプトに、/cgi-bin/test.cgiというURIでアク セスするとFig3のような出力が、/ruby/test.cgiというURIでアクセスすると Fig4のような出力が得られます。このスクリプトは、GATEWAY_INTERFACEとい う環境変数の値を出力していますが、この環境変数はCGIのバージョン(CGIス クリプトのバージョンではなく、CGIという規格(?)自体のバージョンです)を 表します。GATEWAY_INTERFACEの値は、CGIの場合は"CGI/1.1"に、mod_rubyの 場合は"CGI-Ruby/1.1"になります。*3

-- List5 httpd.confの設定

  LoadModule ruby_module /usr/local/apache/libexec/mod_ruby.so

  Alias /cgi-bin/ /usr/local/apache/cgi-bin/
  <Location /cgi-bin>
    SetHandler cgi-script
    Options +ExecCGI
  </Location>

  RubyRequire apache/ruby-run
  Alias /ruby/ /usr/local/apache/cgi-bin/
  <Location /ruby>
    SetHandler ruby-object
    RubyHandler Apache::RubyRun.instance
    Options +ExecCGI
  </Location>

-- List6 test.cgi

  #!/usr/local/bin/ruby

  require "cgi"

  cgi = CGI.new
  print cgi.header("type"=>"text/plain")
  print "GATEWAY_INTERFACE: ", ENV["GATEWAY_INTERFACE"], "\n"

-- Fig3 /cgi-bin/test.cgi

  GATEWAY_INTERFACE: CGI/1.1

-- Fig4 /ruby/test.cgi

  GATEWAY_INTERFACE: CGI-Ruby/1.1

eRubyの利用

mod_rubyでは通常のRubyスクリプトだけでなく、前回紹介したeRubyで記述さ れたファイルも扱うことができます。mod_rubyでeRubyを扱うためには、前回 紹介したerubyかERbが必要です。List7はerubyを利用する場合のhttpd.confの 設定、List8はERbを利用する場合のhttpd.confの設定です。

mod_ruby上でeRubyを利用する場合も、CGIでeruby/ERbを利用する場合と、同 じように利用することができます。もちろん、通常のRubyスクリプトを mod_ruby上で実行する場合とCGIで利用する場合の違いと同じ程度の違いは あります。

-- List7 eruby用のhttpd.confの設定

  RubyRequire apache/eruby-run
  <Location /eruby>
    SetHandler ruby-object
    RubyHandler Apache::ERubyRun.instance
  </Location>

-- List8 ERb用のhttpd.confの設定

  RubyRequire apache/erb-run
  <Location /erb>
    SetHandler ruby-object
    RubyHandler Apache::ERbRun.instance
  </Location>

mod_rubyの落し穴

mod_rubyはCGI互換環境を提供しますが、基本的な仕組みが大きく異なるため、 CGIでは動作するスクリプトがmod_rubyでは上手く動かないということもよく あります。また、mod_rubyの仕組みをきちんと理解していないと、上手く動作 したり、動作しなかったり、という不思議な現象に悩まされることもあります。 ここではmod_rubyを利用する際によく陥りがちないくつかの問題について説明 します。

ライブラリのリロード

mod_rubyでは一度requireされたライブラリは同じインタプリタで再びロード されることはありません。このため、ライブラリの修正を行った場合は、かな らずApacheを再起動してRubyインタプリタを初期化し直してやる必要がありま す。これを忘れると、ライブラリを何度修正しても動作が直らずに、コードを 何度も見直して悩むことになります。

この作業を忘れがちな原因の一つに、Apacheが複数の子プロセスを立ち上げる ため、毎回同じプロセスでスクリプトが実行されるとはかぎらない、というこ とがあります。たとえば、Aという子プロセスとBという子プロセスがあるとし て、fooというライブラリをrequireしているスクリプトがプロセスAだけで実 行されたとしましょう。ここで、fooを変更した後でApacheを再起動せずにス クリプトにアクセスし、プロセスBがリクエストに応答したとします。この場 合、プロセスBのRubyインタプリタはまだ一度もfooをロードしていないため、 変更された後の新しいfooがロードされます。そのため、上手くいったと思っ て安心してしまうわけです。ところが、次にアクセスした時にもしプロセスA が応答すると、古いfooのコードが実行されてしまいます。運悪くプロセスAに 当たる時までは、潜伏期間を持ったウイルスのように、バグは息を潜めている わけです。

一応、mod_rubyにはauto-reload.rbというライブラリが用意されており、この ライブラリをrequireしておくと、require時にファイルの更新時刻を見て、更 新されていれば再ロードを行う、といった処理をさせることができます。しか し、このライブラリは万能ではありません。Rubyのライブラリは同じインタプ リタで二回ロードされた時に正しく動作するとは限らないからです。たとえば、 ENDブロックを利用しているような行儀の悪い(?)ライブラリをrequireしてい た場合、このライブラリを修正するとどんなことが起こるでしょうか。きっと そんなことは知らない方が幸せなのに違いありません。

結局、この問題に対する一番の対処方法は、コードを修正したらApacheを再起 動するという習慣を身に付けることです。習慣は力なり。

GCとファイナライザ

CGIの場合は、スクリプトの実行が終わる度に、Rubyインタプリタの終了処理 が行われます。Rubyインタプリタが終了処理を行う際にはすべてのオブジェク トの寿命は尽きているはずですから、ファイナライザが設定されたすべてのオ ブジェクトに対してファイナライザがかならず実行されます。そういったわけ で、CGIスクリプトの中にはファイナライザの挙動に依存しているものがたま にあります。たとえば、CGI::Sessionを利用しているスクリプトでは、 CGI::Sessionがファイナライザでセッションのクローズ(セッション情報の保 存)をしてくれるため、明示的にクローズを行っていないケースがあります。

しかし、このようなスクリプトはmod_rubyでは上手く動作しません。なぜなら、 mod_rubyではスクリプトの実行が終了した時に、かならずしもGCが呼ばれると はかぎらないからです。いつかリソースが足りなくなった時にはGCが呼ばれ、 回収されたオブジェクトのファイナライザが呼ばれるでしょう。しかし、それ がいつ起こるかということは、神のみぞ知るところです。したがって、ファイ ナライザの挙動に依存したスクリプトは、すべて明示的にその処理を行うよう 修正する必要があります。

私の個人的な意見を言わせていただければ(どうせ返事をする人はいないので 勝手に言いますが)、そもそもファイナライザの挙動に依存したスクリプトは すべて邪悪なスクリプトであり、この世から駆逐されるべきです。なぜなら、 ファイナライザは本来あくまでもリソースの解放のためのものであり、その必 要がなければ(つまり新たにリソースを必要とすることがなければ)呼ばれなく ても構わないものであるはずだからです。前にも書いたような気がしますが、 実際、Javaでは終了時にすべてのオブジェクトのファイナライザが呼ばれると はかぎりません。Rubyもそうであったならどんなに良かったことでしょう。 もしそうであれば、mod_rubyで上手く動作しないスクリプトは、CGIでも動作 しないことになります。したがって、私のところに来る質問は、代りにまつも とさんのところに行くことになったでしょう。(言い忘れましたが、mod_ruby の作者は私です。)

$SAFE

mod_rubyはRubyのセキュリティ機能を利用し、プログラマの不注意によってセ キュリティホールを作ってしまう危険性を低減させています。Rubyでは$SAFE というグローバル変数によってセキュリティレベルを設定することができます。 $SAFEには整数値が設定され、この数値が大きければ大きいほど危険な操作が 行われる可能性は低くなります。

mod_rubyでは$SAFEのデフォルト値は1になっています。$SAFEが1の場合、汚染 された文字列を使ってevalやopenなどの危険な操作を行うと、SecurityError という例外が発生します。汚染された文字列というのは、たとえば環境変数に よって得られたり、標準入力から得られたりする、内容を信頼できない文字列 です。汚染された文字列に対してtainted?というメソッドを呼び出すとtrueが 返ってくるので、ある文字列が汚染されているかどうかは簡単に調べることが できます。また、この汚染情報は伝染性を持っています。たとえば、汚染した 文字列と、汚染されていない文字列を連結すると、汚染された文字列が生成さ れます。

しかし、CGIスクリプトを書いていると、ユーザの入力した文字列から生成し たファイル名によってファイルをオープンしたい、といったことはよくありま す。mod_rubyではこのような場合、そのままではSecurityErrorが発生してし まい、ファイルをオープンすることができません。そこで、untaintというメ ソッドを利用して、文字列を浄化してやります。たとえば、strという文字列 が汚染されているとすると、str.untaintという呼び出しによって、strを汚染 されていない文字列にすることができます。(untaintは破壊的なメソッドであ るということに注意してください。必要ならdupしてからuntaintします。)

最後に

今回はmod_rubyについて簡単に紹介しました。今回説明した範囲では、 mod_rubyもCGIとできること自体はそれほど変わらないという印象を持たれた ことだろうと思います。次回はもう少し踏み込んで、mod_rubyを使ってRubyで Apacheを拡張する方法について説明したいと思います。

参考文献

[1]

modruby, <URL:http://www.modruby.net/>

[2]

The Common Gateway Interface, <URL:http://hoohoo.ncsa.uiuc.edu/cgi/>


*1 このバグはRuby 1.6.5では修正されています。
*2 List4ではまだoptionsの内容を変更してしまう副作用があるため、 本当はoptionsをdupするなどの処置が必要です。
*3 ちなみにmod_perlでは"CGI-Perl/1.1" になります。