mod_perl であほプログラムを作ってみた。
はじめに
自社開発で作ろうとしていた物が実は mod_perl に対応した Web アプリなのですが、そういえば mod_perl ってまともに弄ったこと無かったなぁとか思ったので、風邪も引いて頭も回らないことだし少し脱線するかとばかりに遊んでみました。
世間一般においてはちっともはやらない mod_perl 。かろうじて拾う話題もせいぜい Apache::Registry を用いた既存 CGI の高速化とかその程度の話で、本格的に Perl で Apache モジュールを作ろうみたいな話になると本を買うか英語で情報を探すしかなかったりするのが現状のようです (あるいはおいらの探し方が悪いのかも知れませんが…)。参考にしたのは mod_perl の公式サイトにあるドキュメントでした。
これ全体をぼえ~っと眺めてみても何から手をつければいいのかよくわからないのですが、この辺を見てみるとサンプルを見つけることができたので、これを参考に作ってみることにします。
動かす場所 (URI) を決める
とりあえず弊社の環境では test という名のサブドメインを設け、 http://test.harapeko.jp/aho にアクセスするとプログラムが実行できるということにしてみました。もちろん、サブドメインを設けるには、named (BIND) の設定追加と、 Apache 側の設定が必要です。
サブドメインとか面倒な人は、この辺は読み飛ばしちゃってください。BIND の設定については、ここでは省きます。
Apache 側の設定ですが、あとでここに mod_perl に関する設定も書き加えるので、 mod_perl.c モジュールが見える場所に記述するようにしてください (mod_perl.c を LoadModule している特別な設定ファイルがあるのであれば、その中に記述します)。
<IfModule mod_perl.c> <VirtualHost *:80> ServerName test.harapeko.jp DocumentRoot /var/www/vhosts/test/htdocs ErrorLog logs/test-error_log CustomLog logs/test-access_log common </VirtualHost> </IfModule>
mod_perl モジュールとしてあほプログラムを書く
mod_perl モジュールとしてロードできるのは、@INC のパスが通ったディレクトリに置いてあるモジュールファイルだけです。つまり、通常は Cpan などから正規にインストールしたモジュールだけです。
ただ、 mod_perl の実行中は Apache の設定で ServerRoot に設定されているディレクトリにも @INC のパスが通るようなので、テスト目的でちょっとしたプログラムを書きたいだけならば、この配下にファイルを置いていけばいいでしょう。弊社の環境では /etc/httpd が ServerRoot に設定されているので、例えばモジュールのパッケージ名を MpTest::AhoAho にするのであれば、モジュールファイルは /etc/httpd/MpTest/AhoAho.pm とします。
以下、そのサンプルコードです。
package MpTest::AhoAho;
use strict;
use utf8;
use Encode;
use Apache2::RequestRec ();
use Apache2::RequestIO ();
use Apache2::Const -compile => qw( :common :http );
my $urlEncode;
sub handler {
my $r = shift;
$r->content_type( 'text/html; charset=UTF-8' );
if ( $r->method eq 'GET' )
{
$r->print( encode( 'UTF-8', <<ENDLINE ) );
<html><head><title>あほあほ!!</title></head>
<body>
<h1>あほあほ!!</h1>
<form method="POST" action="${ \ $r->uri }">
<p>どう思いますか? ><input type="text" name="hoge" size="50">
<input type="submit" value="送信"></p>
</body>
</html>
ENDLINE
}
elsif ( $r->method eq 'POST' )
{
my $length = $r->headers_in->{ 'Content-Length' };
if ( $length > 1024 ){
$r->status( Apache2::Const::HTTP_BAD_REQUEST );
$r->print( encode( 'UTF-8', <<ENDLINE ) );
<html><head><title>話が長すぎじゃばっかも~~~ん!!!</title>
<body>
<h1>話が長すぎじゃばっかも~~~ん!!!</h1>
<p>1KB 以内に納めて来い!!</p>
<hr>
<p><a href="${ \ $r->uri }">もう一回?</a></p>
</body>
</html>
ENDLINE
return Apache2::Const::OK;
}
my $buf;
$r->read( $buf, $length ) == $length or die '???';
my $request = $urlEncode->( $buf );
my $comment = "$request->{ hoge } としか言えない貴様はやっぱりあほだ!!!";
$r->print( encode( 'UTF-8', <<ENDLINE ) );
<html><head><title>あほあほあほ!!!</title></head>
<body>
<h1>$comment</h1>
<p>
ENDLINE
for ( 1 .. 1000 )
{
$r->print( encode( 'UTF-8', "$comment " ) );
}
$r->print( encode( 'UTF-8', <<ENDLINE ) );
</p>
<hr>
<p><a href="${ \ $r->uri }">もう一回?</a></p>
</body>
</html>
ENDLINE
}
return Apache2::Const::OK;
}
$urlEncode = sub {
my $url = shift;
my %map;
for my $pair ( split /&/, $url )
{
my ( $name, $val ) = split /=/, $pair;
$name =~ tr/+/ /;
$name =~ s/%([\da-f][\da-f])/pack 'H2', $1/ieg;
$name = decode( 'UTF-8', $name );
$val =~ tr/+/ /;
$val =~ s/%([\da-f][\da-f])/pack 'H2', $1/ieg;
$val = decode( 'UTF-8', $val );
$map{ $name } = $val;
}
return \%map;
};
1;
__END__
URI アクセスに応じてモジュールが動くように設定する
プログラムができたら、それを URI に関連づけます。先の手順でサブドメインの追加を行っていれば、その <VertualHost> ディレクティブの中に、以下のように設定を追加します。
<VirtualHost *:80>
ServerName test.harapeko.jp
DocumentRoot /var/www/vhosts/test/htdocs
ErrorLog logs/test-error_log
CustomLog logs/test-access_log common
# 追加 --ここから
PerlModule APR::Table # 要らないかも。後述
PerlModule MpTest::AhoAho
<Location /aho>
SetHandler modperl
PerlResponseHandler MpTest::AhoAho
</Location>
# 追加 --ここまで
</VirtualHost>
これで設定完了です。 apache2ctl configtest (または /etc/init.d/httpd configtest 等) で設定ファイルの文法を確認したら、 apache2ctl restart (または /etc/init.d/httpd restart 等) で再起動しましょう。
実際に動いている物サンプル
実際に動いているサンプルです。もうなんて言うか激しくごめんなさい。 orz
解説
それでは解説です。まず、 Apache 側の設定ファイル中でキモになる記述は以下の部分です。
PerlModule MpTest::AhoAho
<Location /aho>
SetHandler modperl
PerlResponseHandler MpTest::AhoAho
</Location>
PerlModule はモジュールをロードする宣言で、この行は実は無くても動きます。が、書いておくと Apache の起動時にロードしてコンパイルまで済ませておいてくれるので、多分早くなるんだと思います (多分ってなんだよ>ヲレ)。
PerlResponseHandler は、Apache がクライアントにレスポンスを返す際に呼び出すハンドラモジュールを指定する宣言です。つまり、大雑把に言うと、ここで指定したモジュールの handler メソッドが、 URI アクセス時に呼び出されます。上記のサンプルではこの宣言は <Location /aho> ディレクティブ内に記述されているので、「/aho の URI にアクセスしてきた場合は、応答時に MpTest::AhoAho->handler を実行する」という意味になります。
SetHandler は PerlResponseHandler などのハンドラを指定する宣言を書く場所では必ず書いておく必要のある宣言です。引数は modperl と perl-script の 2種類があって、後者はレガシーなスクリプトとの互換性を重視するためか、プログラム中でそのまま使える print 関数や %ENV などにアクセスできるようになります。今回は敢えてそれができない modperl の方を選んでみました。
次にプログラムですが、内容的には GET アクセス時は質問ページを、 POST アクセス時は罵倒ページを表示するというただそれだけの物です。但し POST したデータが長すぎる (1KB を超える) 場合には自前でエラーを表示するようにしています (ちゃんと 400 Bad Request を返しているので、IE で試すと IE 謹製のエラー画面が表示されるはずです)。
use Apache2::* シリーズはいずれもほぼ必須と考えていいと思います。Apache2::RequestRec と Apache2::RequestIO は、ハンドラ内で最初に受け取るリクエストオブジェクトの各メソッドにアクセスできるようにするためのモジュールで、前者はステータス値や HTTP ヘッダー、環境変数などへのアクセス用、後者はコンテンツの入出力用です。例えば $r->method や $r->uri、 $r->headers_in などを参照したり、$r->content_type で出力用の Content-type: ヘッダーを設定したりするには Apache2::RequestRec が、 $r->print でコンテンツを出力したり、 $r->read で POST を入力したりするには Apache2::RequestIO がそれぞれ必要になります。
Apache2::Const モジュールは各種定数の定義です。引数リストで使用する定数を宣言できますが、その最初に '-compile' と入れておくと、定数名をグローバル名前空間に展開しません (その場合は、 Apache2::Const::HOGE_FUGA としてアクセスします)。
Apache はこのモジュールの handler 関数を呼び出し、最初の引数にリクエストオブジェクトを渡します。そして、自前の処理を実行しつつ、リクエストオブジェクトの各メソッドを適切に呼んであげてから、 Apache2::Const::OK を返してあげればいい、という流れです。あとは、API のリファレンスとにらめっこしつつ読みこなしていただければ、やっていることは大体解ると思います。
$r->headers_in メソッド呼び出しでエラーが発生する場合
ところで、 Apache 側の設定で、以下の行について説明していませんでしたが、
PerlModule APR::Table # 要らないかも。後述
この宣言は、 $r->headers_in メソッドにアクセスした際、以下のようなエラーが発生してしまうのを回避するためのものです。自宅で宅内サーバーにしているマシン@debian では発生しなかったのですが、会社サーバー@centos では発生するので、念のため付記しておきます。
[Mon Dec 29 18:47:51 2008] [error] [client 218.219.204.253] Can't locate object method "FETCH" via package "APR::Table" at /etc/httpd/MpTest/AhoAho.pm line 32.\ n, referer: http://test.harapeko.jp/aho
この解決法については、以下のサイトを参考にしました。
2008 年 12 月 29 日 by 村山 俊之