A Django site.
8月 22, 2008
» Yokohama.pm #2でしゃべってきた

お題は「mod_securityとParse::ModSecurity::AuditLog」でしゃべってきました。YAPCの容赦ない進行に恐れをなして、10分の制限時間で終われるように初めて事前にプレゼンのリハーサルしましたよ。実際には、ちょっと大目に見ていただけたようで、「時間が来たのでぶちっ」ということはありませんでしたが。

肝心のモジュールはまだCPANにあげてません。てか、CPANのアカウント申請してから音沙汰がないのであげられません。ということで、興味のある方はこちらからどうぞ。スライドもありますので、合わせてどうぞ。

結果はどうだったのかというと、「正直、むずかしいですねぇ」という反応が大勢で。ま、あまり期待はしていないというか、受け入れられるには時間がかかるだろうと予測はしているので、簡単にはあきらめません。あきらめない心が大事だってのは、この五輪で教わったしな!くじけない心でこの先も続けます。

super cookie問題は知っているひとはいたけど、public suffix問題については、あまり知られていないようだった。これについては自分でも書く予定。

次に話せる機会があれば、純粋にPerlのネタとかやりたいね。POEとか。securityネタは好きでやってるわけではないから。ま、そんなことを言っていてもしかたがないので、気持ちを切り替えて次のセミナーに向けて頑張ります。前回は即日満席でしたが、まだ空きはありますので、network securityに興味があるという奇特な人はぜひご参加ください。

最後に、会場を提供していただいたデジタルハリウッド様、運営の皆様、ありがとうございました。

» POEによるevent-drivenなPerlプログラミング その2

POEは、Perlで複数の擬似プロセスを作るのが簡単です。Perlで複数の処理を別々に実行するには、forkを使うのが定番でした。

#!/usr/bin/perl
use strict;
use warnings;

my $pid = fork();

if ($pid) {
    # parent
    print "Hi, I'm parent. My PID is $$\n";
    waitpid $pid, 0;
    print "I'm parent. kid $pid exited.\n";
}
elsif ($pid == 0) {
    # kid
    print "Hi, I'm kid. My PID is $$\n";
}
else {
    die "cannot fork() $!";
}
exit;

実行結果は次の様になります。

Hi, I'm parent. My PID is 88225
Hi, I'm kid. My PID is 88226
I'm parent. kid 88226 exited.

Perlのfork()はUnix環境だと、fork(2)システムコールを呼び出します。Perl固有の問題もいくつかありますが、やることはいわゆるUnixプログラミングと同じです。childプロセスはfork()以前の環境をparentと共有し、その後は別々の道を歩きます。

同じことをPOEでやるには、複数のsessionを作成します。

#!/usr/bin/perl
use strict;
use warnings;
use POE;

POE::Session->create(
    inline_states => {
        _start  => sub {
            my ($kernel, $session) = @_[ KERNEL, SESSION ];
            $kernel->alias_set("alice");
            print "Hi, I'm alice. My session ID is " . $session->ID . "\n";
        },
        _stop   => sub {
            print "I'm alice. Exiting.\n";
        }
    }
);

POE::Session->create(
    inline_states => {
        _start  => sub {
            my ($kernel, $session) = @_[ KERNEL, SESSION ];
            $kernel->alias_set("bob");
            print "Hi, I'm bob. My session ID is " . $session->ID . "\n";
        },
        _stop   => sub {
            print "I'm bob. Exiting.\n";
        }
    }
);

POE::Kernel->run();
exit;

実行結果は以下の様になります。

Hi, I'm alice. My session ID is 2
Hi, I'm bob. My session ID is 3
I'm alice. Exiting.
I'm bob. Exiting.

POEではいくつかのお約束がありますが、そのひとつが「fork()を使わないこと」です。POEではプロセスの代わりにsessionを作成します。sessionは、Unixのプロセスモデルによく似ていて、それぞれのsessionは独自のevent、ストレージ($_[HEAP])などを保持します。sessionにはUnixのプロセスIDと同じように、session IDが割り当てられ、それぞれのsessionを区別します。このsessionの分離はPOE::Kernelによってすべて処理され、プログラマは意識する必要がありません。

作成したsession同士で通信させてみます。

#!/usr/bin/perl
use strict;
use warnings;
use POE;

POE::Session->create(
    inline_states => {
        _start  => sub {
            my ($kernel, $session) = @_[ KERNEL, SESSION ];
            $kernel->alias_set("alice");
            print "Hi, I'm alice. My session ID is " . $session->ID . "\n";
        },
        got_message => sub {
            my ($kernel, $session, $msg) = @_[ KERNEL, SESSION, ARG0 ];
            print "(alice got message \"$msg\")\n";
            if ( $msg =~ /how are you\?/i ) {
                $kernel->post("bob", "got_message", "Fine.");
            }
        },
        _stop   => sub {
            print "I'm alice. Exiting.\n";
        }
    }
);

POE::Session->create(
    inline_states => {
        _start  => sub {
            my ($kernel, $session) = @_[ KERNEL, SESSION ];
            $kernel->alias_set("bob");
            print "Hi, I'm bob. My session ID is " . $session->ID . "\n";
            $kernel->post("alice", "got_message", "How are you?");
        },
        got_message => sub {
            my ($kernel, $session, $msg) = @_[ KERNEL, SESSION, ARG0 ];
            print "(bob got message \"$msg\")\n";
        },
        _stop   => sub {
            print "I'm bob. Exiting.\n";
        }
    }
);

POE::Kernel->run();
exit;

実行結果は以下の様になります。

Hi, I'm alice. My session ID is 2
Hi, I'm bob. My session ID is 3
(alice got message "How are you?")
(bob got message "Fine.")
I'm alice. Exiting.
I'm bob. Exiting.

新たにgot_messageというevent handlerを定義し、それぞれ受け取ったメッセージを表示し、aliceのほうは挨拶されたら返事を返します。これと似たような処理をfork()によるIPC(InterProcess Communication)では以下の様になります(Perl Cookbook 16.10より)。

use IO::Handle;
pipe(PARENT_RDR, CHILD_WTR);
pipe(CHILD_RDR,  PARENT_WTR);
CHILD_WTR->autoflush(1);
PARENT_WTR->autoflush(1);

if ($pid = fork) {
    close PARENT_RDR; close PARENT_WTR;
    print CHILD_WTR "Parent Pid $$ is sending this\n";
    chomp($line = <child_rdr>);
    print "Parent Pid $$ just read this: `$line'\n";
    close CHILD_RDR; close CHILD_WTR;
    waitpid($pid,0);
} else {
    die "cannot fork: $!" unless defined $pid;
    close CHILD_RDR; close CHILD_WTR;
    chomp($line = <parent_rdr>);
    print "Child Pid $$ just read this: `$line'\n";
    print PARENT_WTR "Child Pid $$ is sending this\n";
    close PARENT_RDR; close PARENT_WTR;
    exit;
}

ここではpipe()を使って相互に通信しています。このやり方では、お互いのメッセージを待つ間、処理がブロックしてしまいます。非同期に通信する方法もありますが、ややこしくて面倒です(このへんのテクニックは Lincoln D. Steinによる「Perlネットワークプログラミング」が詳しい)。また、通信内容にPerlのオブジェクトを含めたりすることはできません。ややこしいものをやりとりするには通信のためのプロトコルを定めて、お互いがそれを守る必要があります。

POEによる例では、postによってeventは非同期に処理されます。bobはaliceにメッセージをpostしたあとに別の処理を実行できます。postされたeventはPOE::Kernelがよきに計らってくれます。postする内容は任意のPerlデータを含められますので、objectを投げることだってできます。aliceはメッセージを受け取り、メッセージが挨拶であればbobに返事を返しています。postの引数は$_[ARG0]に入っています。複数の引数を受けとるには、ARG0、ARG1などを使うか、任意の引数を受けとるのであれば、@_[ARG0..$#_]を使います。

eventはsessionに送るmessageです。POE::Kernelはeventを発生させ、sessionはeventに応じて処理を実行します。session同士は、eventを通じて通信します。eventを送られたsessionはeventに応じたevent handlerを実行します。event handlerのことをPOEではstateと呼ぶことがあります。このeventとevent handlerを対応付けるのがinline_statesです。POE::Session->createの重要な役割は、この対応付けです。対応付けの方法はいくつかあり、coderefを使うinline_state、objectのmethodを対応させるobject_states、package methodを対応させるpackage_statesがあります。それぞれメリットデメリットがあります。

sessionはsession IDによって区別されますが、sessionにはわかりやすいaliasをつけることができます。それを行っているのが$kernel->alias_setです。これはUnixにおけるシステムコールみたいなもので、実際の処理はPOE::Kernelが行います。sessionの管理はすべてPOE::Kernelが面倒を見ます(こうした例は、POEが仮想OSと呼ばれる理由です)。aliasをつけるとeventを発行する対象のsessionのIDを意識せずにeventを発行できます。aliasとsession IDの名前解決もPOE::Kernelがよきに計らってくれます。

8月 19, 2008
» POEによるevent-drivenなPerlプログラミング その1

POEはevent-drivenなmultitasking frameworkです。最近では名前こそ知られてきたものの、(上級者を除くと)広く使われてはいません(ただし歴史は長いし、採用事例もたくさんあります)。なぜかというと、かなーり奥が深くて、ドキュメントが膨大で、しかもソースコードの上から順に実行される一般的なPerlプログラムと大きく異なるイベント駆動なフレームワークだからでしょう。自分も、「なんかPOEってすごいらしいよ」と耳にしてから少しずつ勉強していたんですが、どーも理解が進まなくて苦労しました。そんな最初のハードルを低くするような文書が欲しかったので、書いてみることにします。

POEは、event-drivenなプログラムに最適です。event-drivenなプログラムとは、何らかのイベントが起きたらなんらかの処理をする、そういうプログラムです。例えば、GUIアプリケーションならユーザがボタンをクリックしたらある処理をするとか、IRCのbotなら自分宛にメッセージが来たら何らかの処理をするとか、イベントに対応する処理を書いていくプログラムです。

POEは、マルチタスク処理にも便利です。特定の処理ごとにプログラムの分割ができるので、擬似threadなPerlプログラムを書けます。これはsessionと呼ばれ、POEのコアであるPOE::KernelによってCPUの使用時間が割り当てられ、(cooperativeな - つまり各sessionが協力する限りにおいて)multitaskingが可能になります。IRCクライアントなら、IRCサーバとのやりとりを担当するsessionとユーザからの入力を担当するsessionに分割して、それぞれを分離したかたちでプログラムを書けます。それぞれのsessionはイベントを通じて協調します。

POEは、non-blockingな処理が得意です。例えば、サーバにコマンドを投げて、その結果を取得するといった処理をフツーに書いてしまうと、すぐに返事が返ってくればいいのですが、時間のかかる処理ならサーバにコマンドを投げた後にサーバからの結果を待つ間、プログラムの処理は止まってしまいます。そこでサーバから結果が返ってきたかどうかをpollして、まだ返事がないようなら別の処理をするという風に書かなければいけないのですが、なかなか面倒です。こうした処理をPOEでは「サーバから返事があった」というイベントに応じるevent handlerを書いておけば、返事があったときに自動的にそのevent handlerが実行されます。待っている間も別の処理が実行されます。

POEは、分散処理にも使えます。sessionは同一POE::Kernelで実行されるsessionとしかメッセージをやりとりできませんが、POE::Component::IKCを使うと、異なるkernel間でErlangっぽくメッセージをやりとりできます。

多くのPerlプログラムは上から順に実行していきます。サブルーチンやオブジェクト指向とかの違いはあれど、基本は一緒です。

#!/usr/bin/perl

sub init {
...
}

sub do_this {
...
}

sub do_that {
...
}

sub finished {
...
}

init();
do_this();
do_that();
...
finished();
exit;

これがPOEになると一変します。

#!/usr/bin/perl
use strict;
use warnings;
use POE;

sub _start {
  my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
  print "Session ", $session->ID, " has started.\n";
  $kernel->yield('do_this');
}

sub do_this {
  my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
  # do something...
  print "Done!\n";
}

sub _stop {
  my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
  print "Session ", $session->ID, " has finished.\n";
}

POE::Session->create(
  inline_states => {
    _start => \&_start,
    do_this => \&do_this,
    _stop => \&_stop,
  }
);

POE::Kernel->run();
exit;

サブルーチンの定義を除くと、POE::Session->createとPOE::Kernel->run()しか実行していません。それぞれ、sessionを作成し、kernelを起動しているだけです。何が起きているかというと、作成されたsessionはイベントごとに何を実行するかを定義しています(inline_states)。_startと_stopはsession management eventといって、それぞれsessionの作成および終了時に発生するイベントです。do_thisはユーザ定義の独自のイベントです。それぞれのイベントにはあらかじめ定義したcoderefが割り当てられています。_startイベントの終わりでは、yieldでdo_thisイベントを呼び出しています。do_thisイベントでは何らかの処理をして、することがなくなったのでkernelは_stopイベントを呼び出します。つまり、_startがdo_thisを、do_thisが_stopを、というふうにイベントがつながっているわけです。

各event handlerの最初に見慣れない@_があります。

my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

この表記はPOE特有のものです。通常のPerlのサブルーチンやメソッドでは

sub foo {
  my @args = @_;
  ...
}

# or...

sub bar {
  my ($self, @args) = @_;
  ...
}

と書くことが多いですが、それと同じようなものです。KERNELやHEAPはそれぞれ@_におけるindexで、こうすることで必要な@_の要素を任意の順番で取り出すことができます。引数を受けとるには以下の様になります。

my ($kernel, $heap, $parameter) = @_[KERNEL, HEAP, ARG0];

POE::Sessionの@_にはいろんな値がつまっていますが、必要なものだけ取り出せばOKです。複数のsessionをつくるのも簡単です。最後の部分をforで回してみましょう。

for (1..10) {
  POE::Session->create(
    inline_states => {
      _start => \&_start,
      do_this => \&do_this,
      _stop => \&_stop,
    }
  );
}

各sessionがそれぞれ処理をしているようすが表示されます。

POEは一見とっつきにくいですが、とっても便利なフレームワークで、POE-wayに慣れると実に自然にコードが書けます。そう、Perlのように。Perlもlist contextやら@INCやらと、初めはとっつきにくいのですが、Perlな思考が身につくと(Perl脳ともいう)、Perlらしいコードが書けるようになったはずです。ぜひ挑戦してみてください。最後に参考になった文書などをあげておきます。

  • POE固有の概念をおおまかに理解するならBeginners_Guide
  • カンファレンスのスライドとかはTutorials
  • 基本を押さえたサンプルならdistributionに含まれるsamplesディレクトリ
  • すぐ動く実用的なサンプルならPOE_Cookbook
  • 付属のman。長いしたくさんあるので、POE、POE::Kernel、POE::Sessionをとりあえず読めばいい。最初に理解できなくてもくじけないこと。コード書いているうちにわかるようになります

7月 21, 2008
» File::Streamで正規表現をrecord separatorにする

mod_securityのaudit logをいい加減どうにかしたいと思って、ちょろっと調べた感じだと、Java-basedなツールとか非freeなツールとかしかないことが判明。ちょっとしたサマリとかが書きたいだけなのに。modsec2sguilに含まれるModsecAlert.pmが求めているものに近いんだけど、あまり気にくわない。while使ってるけど、1レコードごとに処理できないとか、パース処理がきれいじゃないとか。

なら、自分で作るかとか思ったんだけど、mod_securityのログはちょっと特殊で、

--c67eae67-A--
(log header)
--c67eae67-B--
(HTTP headers)
--c67eae67-C--
(HTTP message body)
--c67eae67-F--
(HTTP response)
--c67eae67-H--
(audit log)
--c67eae67-Z--

確かにパースするのが面倒そう。しばらく考えてると、「よーするに、–\w+-Z–をinput record separetorにしてwhileで回せばいいんじゃね?」と気がつく。でも$/には文字列しか渡せない。うーむ。で、CPANあさってたら見つかったのが、File::Stream。まさに、やりたいことがそのまんまできる。

my $stream = File::Stream->new(
    $fh,
    read_length => 1024,
    separator => qr/--\w+-Z--\n\n/
);

while (<$stream>) {
    # do something...
}

Perl 5.10だったら幸せになれたりするのかな。

5月 18, 2008
» Perlモジュールのインストールがむずかしい?

「Mooseはインストールがむずかしい」という発言をYAPCのあるセッションで耳にした。こういう不満を聞く度に思うのは、「それって野良buildしてんの?」「ってか、それってOSのパッケージ管理システムがタコなだけなんじゃないの?」という疑問。「依存関係が多くてインストールが大変」とかいうけど、どうやってインストールしてるんですかね。少なくともPerlのmoduleに関して言えば、インストールが大変だったことはない。

FreeBSDやOpenBSDでは、portinstall p5-Mooseもしくはpkg_add p5-Mooseで一発インストールだし、Gentooのg-cpanに至ってはPortageにないmoduleですら自動的にebuildを作成してインストールしてくれる。インストールがむずかしいからこそ、苦労するのはmaintainer1人でいいんじゃないでしょうか。Windowsあたりで苦労するのは理解できるのだけど、他のdistributionのPerlサポートってそんなにダメダメなんですか。「依存関係が多すぎ」という批判に対してはすでにjrockwayが反論している(Myth: Moose is an unnecessary dependency)。それとも、特権がない環境で$HOMEにインストールするのが大変ということなんだろうか。

cpanではなく、パッケージ管理システムを使ってインストールするメリットは、インストール、アンインストール、アップデートが容易になることだけでなく、package maintainerのreviewが入るということもある。パッケージ管理システムに入っていない場合でも、自分は野良portを書いてインストールする。ついでにsend-prしてしまえば、他の人も幸せになるし。そうやって使う人が増えると、bugが上がってくる可能性だって増えるわけだし、いいことずくめだと思うのですが。

UPDATE: 「rootを持ってる人にとってはそれでいい。でも、だれもがrootを持っているわけではないし、クロスプラットフォーム対応しながら他人にインストールしていただかないといけない人もいるということ」 あー、理解できました

4月 14, 2008
» 「日本人しかつかわないだろうから」というのはASCIIしか頭にない開発者より(ry

  • 日本人しか日本語処理をしない
  • 非日本人が日本語サイトの開発をしているはずがない

3月 21, 2008
» Mooseによるオブジェクト指向Perl

Mooseは”an extension of the Perl 5 object system”だそうで、なんかすごいらしい。使ってみるとattributeやaccessorの追加も簡単で、型の確認も自動でやってくれてrobustなclassを作るのに便利。attributeがarrayやhashのreferenceだったら、contextに応じて自動的にdereferenceしてくれるとか、extendやoverrideがやりやすいとか、他にもいろいろあるらしい。Mooseは自動的にhash-basedのobjectを作ってくれる。methodを追加する方法も古典的なPerl5のOOPと同じ。

package Foo;
use strict;
use warnings;
use Moose;

sub say {
    my ($self, $str) = @_;
    print "$str\n";
}

1;
#/usr/bin/perl
use strict;
use warnings;
my $bar = Foo->new();
$bar->say('Hello world');

この辺がPerl6からアイデアを借りてきたけどPerl6ではないよ、ということらしい。中でもcoercionという仕組みが便利だった。

ネットワークのホストをclassにしてみる。具体的には$host->ipとかでIP addressを取ってこれるようにしてみる。

> mkdir MyMoose; cd MyMoose
> mkdir lib t

まずはテストから。

> vim t/Host.t
#!/usr/bin/perl
use strict;
use warnings;
use FindBin qw($Bin);
use lib "$Bin/../lib";
use Test::More qw{ no_plan };

BEGIN {
    use_ok 'Host'
};

テストの実行。当然失敗する。

> perl t/01_Host.t
not ok 1 - use Host;
#   Failed test 'use Host;'
#   at t/01_Host.t line 9.
#     Tried to use 'Host'.
#     Error:  Can't locate Host.pm in @INC (@INC contains:...

use Moose!

> vim lib/Host.pm
package Host;
use strict;
use warnings;
use Moose;

1;

これでテストが通る。

> perl t/01_Host.t
ok 1 - use Host;
1..1

new()はMooseが用意してくれる。

どんなふうにこのclassを使いたいのかを考える。ip()でNettAddr::IP objectをsetもしくはgetしたいので、そのようにテストを書く。

> vim t/01_Host.t
#!/usr/bin/perl
use strict;
use warnings;
use FindBin qw($Bin);
use lib "$Bin/../lib";

use Test::More qw{ no_plan };
use NetAddr::IP;

BEGIN { use_ok 'Host'; };

ok my $h = Host->new, 'new()';
ok $h->can('ip'), 'can ip()';
ok $h->ip( NetAddr::IP->new('10.10.10.1/32') ), 'set ip()';
ok $h->ip->isa('NetAddr::IP'), 'isa NetAddr::IP';
is $h->ip, '10.10.10.1/32', 'get ip()';

実際にaccessorを作成する。accessorを作るにはhasキーワードで。accessorを自動的に作ってくれる。isをrwにしてsetterとgetterの両方を作る。

> vim lib/Host.pm
package Host;
use strict;
use warnings;
use Moose;

has 'ip' => ( is => 'rw' );
1;

テスト。

> perl t/01_Host.t
ok 1 - use Host;
ok 2 - new()
ok 3 - can ip()
ok 4 - set ip()
ok 5 - isa NetAddr::IP
ok 6 - get ip()
1..6

これだとaccessorを作るのがちょっとだけ簡単になっただけ。Mooseが便利なのはここから。isaを使うと渡された値が必ずある型(type)であることを確認してくれる。typeにはクラス名もしくはMooseのtypeを指定する。defaultで定義されているtypeはMoose::Util::TypeConstraintsを参照。

has 'ip' => ( is => 'rw', isa => 'NetAddr::IP' );

これでNetAddr::IP以外が渡されるとcroakしてくれる。自分で明示的にtypeを定義するにはsubtypeを使う。

package Host;
use strict;
use warnings;
use Moose;
use Moose::Util::TypeConstraints;

subtype 'NetAddr::IP'
    => as 'Object'
        => where { $_->isa('NetAddr::IP') };
has 'ip' => ( is => 'rw', isa => 'NetAddr::IP' );
1;

でも、$h->ip( ‘10.10.10.1/32′ )って書いて、自動的にNetAddr::IP objectに変換してくれたほうが便利じゃね?ということで、テストを追加。

ok $h->ip( '10.10.10.1/32' ), 'set ip()';
ok $h->ip->isa('NetAddr::IP'), 'isa NetAddr::IP';

このテストも成功するように実装する。

入力値をあるtypeから別のtypeに変換するには、coerceを使う。

package Host;
use Moose;
use Moose::Util::TypeConstraints;
use NetAddr::IP;

subtype 'NetAddr::IP'
    => as 'Object'
        => where { $_->isa('NetAddr::IP') };
coerce 'NetAddr::IP'
    => from 'Str'
        => via { NetAddr::IP->new($_) };
has 'ip' => ( is => 'rw', isa => 'NetAddr::IP', coerce => 1 );
1;

coerceキーワードで、渡された値がStr type(文字列)だった場合に、viaで指定したblockで返される値に変更する。type constraint(NetAddr::IP objectなのかどうか)はこの変更された値に対して行われる(つまり、coercionはtype constraintより前に行われる)。blockの中で渡された値にアクセスするには$_を使う。

これでtestも通るようになった。

ok 1 - use Host;
ok 2 - new()
ok 3 - can ip()
ok 4 - set ip()
ok 5 - isa NetAddr::IP
ok 6 - get ip()
ok 7 - set ip()
ok 8 - isa NetAddr::IP

試しに変なもの(例えばarray refとか)をip()に渡してやると、

Attribute (ip) does not pass the type constraint because: Validation failed for 'NetAddr::IP' failed with value ARRAY(0x8435088)

とエラーを吐いてくれる。

今回はわかりやすいようテストを書いたけど、「Mooseのキモはこうしたテストを書かなくても自動的にやってくれることだ」とjrockwayは書いています(Myth: Moose is an unnecessary dependency)。

使ってて気がついたことは、あくまでblessed hashのobjectなので中身をいじろうと思えばできてしまうこと、動作が遅いこと。たくさんのobjectを作るとかCGIで使うのには向かない。そういうのはClass::Accessorとかのほうがいいっぽい。

Mooseのマニュアルには

I built Moose because I was tired of writing the same old boring Perl 5 OO code, and drooling over Perl 6 OO. So instead of switching to Ruby, I wrote Moose :)

といかにもPerlらしいコメントが。

3月 12, 2008
» Perlをこれから学ぶ人たちへ送る言葉

  1. Perl 5.6.x以降を使え(可能なら5.8.x以降)
  2. 常に”use strict;”を使うこと
  3. 常に”use warnings;”を使うこと
  4. 信頼できないデータを扱うなら”-T”オプション(taint mode)を使うこと
  5. 警告を無視しないこと。 “use diagnostics;”と”perldoc perldiag”を参照のこと
  6. perlcriticを使うこと。Perl Best Practice嫁
  7. perltidyを使うこと
  8. “perldoc perlfaq”を読むこと。最初のうちに感じる疑問のほとんどはそこに回答が書いてある
  9. 問題はCPANで解決されている!

他に何かありますか?

3月 10, 2008
» Test::SMTP 再び

Test::Baseで書き直してみた。XCLIENTをサポートしているので、Postfixを使っていればさらに幸せになれる。Net::SMTPはnew()した時点でconnectしてしまう(objectだけを作って使いまわしができない)ので、package variableを使ってしまった。いまいち。メッセージを実際に送るテストは別のモジュールを書いた方がいいのかも。

UPDATE: xclient sectionがあれば自動的にXCLIENTを発行するようにした。

package Test::SMTP;
use strict;
use warnings;
use Test::SMTP::Filter;
ese Test::Base -Base;
our $hostname;
our %option;
our @EXPORT = qw{ server_is option_is };

filters {
    envelope    => [qw{ lines chomp envelope }],
    status      => [qw{ chomp regexp }],
};

sub server_is {
    $hostname = $self;
    return;
}

sub option_is {
    %option = ($self, @_);
    return;
}
1;
__END__

=head1 NAME

Test::SMTP - Test SMTP server with Perl

=head1 SYNOPSIS

    use Test::SMTP;
    plan tests => 1 * blocks;

    server_is 'host.example.org';
    option_is
        Hello => 'myname.example.org',
    ;

    run_like envelope => 'status';
    run_like xclient_envelope => 'status';
    __END__
    === Postmaster exists
    --- envelope
    foo@example.net
    postmaster@example.org
    --- status
    250
    === is NOT open relay
    --- envelope
    foo@example.org
    bar@example.net
    --- status
    5\d{2}
    === XCLIENT support
    --- envelope
    foo@example.org
    bar@example.org
    --- xclient
    ADDR=10.10.10.1
    NAME=host.example.com
    --- status
    5\d{2}

=head1 DESCRIPTION

It's boring and error prone to test SMTP server using telnet(1). Test::SMTP
automates SMTP server test.

=head1 EXPORTED FUNCTIONS

=head2 server_is($hostname)

Specify the name of the SMTP server. Must be invoked before any test.

=head2 option_is(%option)

%option is passed to the constructor for a new L<Net::SMTP> object.

=head1 DATA SECTION

=head2 envelope

Used to specify the envelope.

    --- envelope
    $envelope_from
    $envelope_to

=head2 status

Regular expression for expected status code.

    --- status
    2\d+

=head2 xclient

Issue C<XCLIENT> command after initial SMTP greeting. C<XCLIENT> is a
non-standard SMTP extension for overriding client-related session attributes,
defined at: L<http://www.postfix.org/XCLIENT_README.html>. Very useful for
testing ACL. As of this writing, only Postfix version 2.1 or later support
C<XCLIENT>. To enable C<XCLIENT> support, see postconf(5).

    --- xclient
    ADDR=10.10.10.1

=head1 SEE ALSO

L<Test::Base>, L<Net::SMTP>, L<http://www.postfix.org/XCLIENT_README.html>

=head1 BUGS

After C<XCLIENT>, you cannot use different host name in the second SMTP
greeting.

=head1 TODOs

=over

=item Support C<DATA> SMTP command.

=item Support SMTP Auth.

=back

=head1 AUTHOR

Tomoyuki Sakurai <cherry@trombik.org>

=head1 COPYRIGHT

Copyright (c) 2006. Tomoyuki Sakurai. All rights reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

See http://www.perl.com/perl/misc/Artistic.html

で、filter。

package Test::SMTP::Filter;
use strict;
use warnings;
use base 'Test::Base::Filter';
use Net::SMTP;

sub envelope {
    my ( $self, $from, $to ) = @_;
    my $status;

    # FIXME Using package variable is bad
    my $smtp = Net::SMTP->new( $Test::SMTP::hostname, %Test::SMTP::option );
    $status = $smtp->code;

    # return immediately if status code is not 2xx...
    if ( int( $status / 100 ) != 2 ) {
        $smtp->quit;
        return $status;
    }
    if ( defined $self->current_block->xclient ) {

        # XCLIENT support...
        if ( $smtp->message !~ /XCLIENT/xms ) {
            warn "the server doesn't support XCLIENT\n";
            return;
        }
        my $xclient_arg
            = join( q{ }, split( /\n/, $self->current_block->xclient ) );

        # issue XCLIENT, override client-related session attributes...
        # http://www.postfix.org/XCLIENT_README.html
        $smtp->command( 'XCLIENT', $xclient_arg );
        my $response = $smtp->response;
        if ( !$smtp->ok ) {
            my $message = $smtp->message;
            $smtp->quit;
            warn "XCLIENT failed: ", $message, "\n";
            return;
        }

# after XCLIENT, the server resets state to the initial SMTP greeting protocol stage...
# XXX what if a user want to use different name in second HELO?
        $smtp->hello( $Test::SMTP::option{Hello} );
        $status = $smtp->code;
        if ( int( $status / 100 ) != 2 ) {
            $smtp->quit;
            return $status;
        }
    }
    $smtp->mail($from);
    $status = $smtp->code;
    if ( int( $status / 100 ) != 2 ) {
        $smtp->quit;
        return $status;
    }
    $smtp->to($to);
    $status = $smtp->code;
    $smtp->quit;
    return $status;
}

1;
__END__

3月 6, 2008
» 質問と議論はコードで

自分の場合だと「設定ファイルで」なんですが。「とりあえずpostconf -nの結果を書け、話はそれから」みたいな。具体的な設定やコードが冗長過ぎるということはあるのだけど、無用な誤解を避けるのには大事。

loginが必須(cookieが必須)の場合、formの改竄を検知する方法として、以下のような解決策ってどうでしょうか。共通の秘密として$c->sessionidを使うのがよろしくないなら、別のユニークかつランダムなキーでもいい。よーするに、viewでformに[% sessionid %](もしくは別のユニークかつランダムなキー)を書いておかないと、submitがそもそも成功しないようにする、というやり方。CPANに同じ問題を解決しようとしているモジュールがあるのは知っているんですけど。

# in Root.pm
sub auto : Private {
    # pass session id to every view so that sessionid is available in every form
...
    if ( $c->sessionid ) {
        $c->stash->{sessionid} = $c->sessionid;
    }
    if ( lc $c->req->method eq 'post' ) {
        my $form_sessionid = $c->req->parameters->{sessionid};
        if ( $form_sessionid ne $c->sessionid ) {

            # the form is not generated by the same session...
            # XXX explain why the user is redirected to /login
            $c->log->error('sessionid is not valid');
            $c->logout;
            $c->res->redirect( $c->uri_for('/login') );
        }
    }
}
[% # in some/form.tt2 %]
<form method='post'>
...
  <input name='sessionid' type='hidden' value='[% sessionid %]'>
</form>

3月 2, 2008
» Net::Patricia

他人のコードにNet::Patriciaという見慣れないモジュールがあったので、「何これ」って聞いてみたらrouting lookupに使うモジュールらしい。PODを読んでもいまいちどんな目的で使うのかよくわからなかった。Google Code Searchとかで調べてもBGP関連で使われているだけで、それ以外で何に使えばいいかしばらく考えてた。でも、よく考えたらあるIP addressがどのサブネットに属していて、そのサブネットの情報(例えばVLAN ID)を引っぱってきたい、というのはよくあるかも。確かにNetAddr::IPとかでは効率が悪そう(無理ではないけど)。

#!/usr/bin/perl
use strict;
use warnings;
use Net::Patricia;

my $address = shift;
my %vlan_id_of = (
    "192.168.0.0/16"    => 1,
    "192.168.10.0/25"   => 2,
    "192.168.10.128/25" => 3,
);

my $pt = Net::Patricia->new;
foreach my $subnet ( keys %vlan_id_of ) {
    $pt->add_string($subnet, $vlan_id_of{$subnet});
}
print "$address belongs to VLAN " . $pt->match_string($address) . "\n";

なんか、いまいち便利そうに見えないサンプルなので、もう少しだけひねってみた。

NetAddr::IPにvlan()メソッドを追加する。

package My::NetAddr::IP;
use strict;
use warnings;
use base 'NetAddr::IP::Lite';
use base 'Class::Accessor::Fast';
__PACKAGE__->mk_accessors(qw{ vlan });

1;

add_string()の第2引数には任意のscalarを代入できる。なので、My::NetAddr::IPオブジェクトを入れてみる。

#!/usr/bin/perl
use strict;
use warnings;
use lib 'lib';
use Net::Patricia;
use My::NetAddr::IP;
my $address = shift;

my %vlan_id_of = (
    "192.168.0.0/16"    => 1,
    "192.168.10.0/25"   => 2,
    "192.168.10.128/25" => 3,
);

my $pt = Net::Patricia->new;

foreach my $subnet ( keys %vlan_id_of ) {
    my $netaddr = My::NetAddr::IP->new($subnet);
    $netaddr->vlan( $vlan_id_of{$subnet} );
    $pt->add_string( $subnet, $netaddr );    # add_string($addr, $any_scalar)}

# print the number of available IP address in the subnet where $address belongs to
print $pt->match_string($address)->num, "\n";

# extended property; VLAN ID
print $pt->match_string($address)->vlan, "\n";

どうでしょ。便利そうに見えませんか。

2月 25, 2008
» Perl初心者殺すにゃ刃物はいらぬ

便利なmodule名をささやけばよい

2月 23, 2008
» もっと早くに知りたかったDBIx::Class::WebForm

ちょっと煮詰まったので、他人のコードを読むことに。CatalystのWikiにはExampleなるセクションがあって、いくつかアプリケーションが紹介されているのだけど、ことごとくダメ(動かない、確かにCatalyst使っているけどなんか違う、ソースがダウンロードできないとか)。mojomojoが参考になりそう。

で、読んでいるとコメントをデータベースに突っ込む際に

$c->model("DBIC::Comment")->create_from_form($c->form);

なんてしてる。えー、そんだけでいいのか。

なんつーか、こういうショックってVimに似てる(Tip #1: the super star)。

2月 20, 2008
» Catalyst::Manual::Tutorial::UTF8というのはどうか

個人的に、日本語というか非ASCII文字を扱う場合における(バッド)ノウハウのPODを作りかけてて、こういうものはいろんな人の手によって作って、みんなで共有すべきなんじゃないかと思い始めた。別にCatalyst::Manual::Cookbook::UTF8でもいい。Catalyst::Manual::Tutorial::Multibyteでもいいけど、いまどきUTF8以外をわざわざ採用する理由ってあまりないような(すくなくともPerlでは)気がする。別にCatalyst::という名前空間であるべき理由はないかもしれないけど、プログラマがi18nとかにまず直面するのはWebアプリなんじゃないかな。しかも、いろんな部分で知らなくてはいけないことが多いし。ここら辺のノウハウって、Perl使いの日本人が得意としてそうだし。正直自分の経験から生まれたノウハウはないから、散乱してるblogのpostとかを理解して、sample codeに反映してPODを書いてる。

つーことで、誰か(一緒に)書きませんか。

2月 13, 2008
» DBIx::Class::SchemaでSQLを生成できるらしい

DBIx::Class::Schema::Loaderで既存のデータベースからDBIx::Class::Schemaを作成できるなら、DBIx::Class::SchemaからスキーマのSQLを生成することだって可能なんじゃないかと思って、DBIx::Class::Schemaのマニュアルを眺めていたら、create_ddl_dirなんつーメソッドがあった。EXPERIMENTALだけど。実行にはSQL::Translatorが必要。しかもDBIx::Class::Schema::Versionedなんつーものもあるんですか。RoRっぽくスキーマをアップグレードできるらしい。とりあえずMySQLとPostgreSQLのSQLが欲しかったので、create_ddl_dirでdumpしてみた。

#!/usr/bin/perl
use strict;
use warnings;
use lib qw{ lib };
use My::Schema;
# necessary to compose proper filename
my $schema = My::Schema->connect(
    [ 'dbi:Pg:dbname=mydatabase' ],
);

$schema->create_ddl_dir(
    [ qw{ PostgreSQL MySQL } ],
    $schema->VERSION,
    'sql_dir',
);

実行後にsql_dir/My-Schema-0.01-{PostgreSQL,MySQL}.sqlというファイルが生成される。当然ながらstorage固有の機能が使われていると別のstorageではうまくいかない。

2月 10, 2008
» Class::InsideOutでinside-outオブジェクト

Perl Best Practiceではinside-outオブジェクトが推奨されているけど、肝心のdconway氏によるClass::Stdはあまり評判が良くない。thread-safeではないというのは許せても、開発が止まってるっていうのは痛い。いくつかalternativesはあるようだけど、Class::InsideOutが良さそう。

ということで、objectのお約束であるPeopleを書いてみた。DESTROYも書かなくていいし、accessorの定義時にset_hookも一緒に書ける。便利だ。しばしばモジュールは予想もできない使われ方をする(そして、それを直すのは約束を破った本人ではない)ので、モジュールの作者以外がモジュールを使う場合はinside-outにしたほうがいい、と最近思うようになった。

#!/usr/bin/perl
use strict;
use warnings;
package People;
use Class::InsideOut qw{ public readonly private register id };

public name => my %name;    # r/w accessor
public
    age => my %age,
    {
    set_hook => sub {
        /^\d+$/x or die "age must be int";    ## no critic
        }    # called when $obj->age($age)
             # automatically rethrow proper error
    };

readonly secret => my %secret;    # w/ accessor but read-only

sub new {
    my ( $class, %arg_of ) = @_;

    my $self = register($class);    # $self is anon scalar ref

    # as usual..
    $self->name( $arg_of{name} ) if exists $arg_of{name};

    # fast but wihout set_hook check
    # $age{id $self} = $arg_of{age} if exists $arg_of{age};
    $self->name( $arg_of{name} ) if exists $arg_of{name};

    if ( exists $arg_of{age} ) {
        $self->age( $arg_of{age} );
    }
    if ( exists $arg_of{secret} ) {

        # even if inside the class, you cannot do $self->secret('foo')...
        $secret{ id $self } = $arg_of{secret};
    }
    return $self;
}

1;

2月 8, 2008
» PerlでTCP 3way-handshake

3wayを生ソケットでやるのは基礎?

「TCPの3wayを生ソケットでやるのは、セキュリティエンジニアの基礎である」という結論に至りました(ホントか?(笑))。ちなみに「tcp 3way raw socket」でググってみたのですが、日本語サイトは数えるほどしかヒットしませんでした(基礎なのに…)。まぁ基礎かどうかはおいといても、SYN floodというようなTCPの3wayを突く攻撃などもあるので、一度くらいはやってみても損はないかも…。

でも、raw IP socketってL4より上のレイヤしかいじれない。FreeBSDのip(4)によると”Outgoing packets automatically have an IP header prepended to them (based on the destination address and the protocol number the socket is created with), unless the IP_HDRINCL option has been set.”なので、IP headerやL2 frameはkernelがよきに計らってくれる。んで、remoteから攻撃可能なkernelの脆弱性って、L3以下にあることが多くない(最近だとIPv6まわり)?L2のMITMも珍しくないし、L2に対する攻撃だってあるので、L2から理解しておくのも大事。でも、ネットワークの低レベルなシステムコールはsocketが基本なので、L2をいじるにはsocketを使わずになんとかしないといけないんだけど。

で、Perlでどうすればいいかというと(これが本題だったりして)、Net::Packetが便利。内部でNet::Pcapを使っていて、生のframeを読み書きできる。L2を理解するということは、PPPのframeからIPのパケットを書き出すとかもできる(あくまでネタのなので、パフォーマンスがとかは気にしない)。TCP 3way-handshakeはこんなかんじ(実際にはkernelがRSTを返さないようにする必要がある。FreeBSDならsysctl net.inet.tcp.blackhole=1)。

#!/usr/bin/perl
use strict;
use warnings;
use Net::Packet;
use Net::Packet::Consts qw(:tcp);

# send SYN
my $ip = Net::Packet::IPv4->new(
    dst => '192.168.10.1',
);
my $tcp = Net::Packet::TCP->new(
    dst => 80,
    flags   => NP_TCP_FLAG_SYN,
);
my $frame = Net::Packet::Frame->new(l3 => $ip, l4 => $tcp);
my $isn = $tcp->seq;

print "send SYN\n";
$frame->send;

# wait SYN+ACK
until ($Env->dump->timeout) {
    last if $frame->recv;
}

if ( $frame->reply->l4->flags == NP_TCP_FLAG_SYN + NP_TCP_FLAG_ACK ) {
    print "got SYN+ACK\n";
}

# send ACK
$tcp->seq($isn + 1);
$tcp->flags(NP_TCP_FLAG_ACK);
my $his_isn = $frame->reply->l4->seq;
$tcp->ack($his_isn + 1);

$frame = Net::Packet::Frame->new(l3 => $ip, l4 => $tcp);
print "send ACK\n";
$frame->send;

2月 6, 2008
» PerlによるDNSのテスト - Test::Base

perldoc Test::Baseしてもさっぱりピンと来なかったけど、”use Test::Base;” (PDF 172KB)を読んだら5秒で理解できた。いろんなひとがいろんな方法で説明するのは大事です。filterが期待する入力値(scalarなのかlistなのか)とfilterの出力を理解するのに手間取ったけど。まずはfoo.tを書いて、徐々にfactor outするのがいい。書けば書くほどごちゃごちゃしていくのがTest::More、逆にテストがキレイになっていくのがTest::Base。それにTest::Builderは、使い勝手がいいとはあまり言えないし。

package Test::DNS;
use strict;
use warnings;
use Test::Base -Base;
use Net::DNS;

our $res = Net::DNS::Resolver->new( searchlist => [], );
our @EXPORT = qw{ nameservers searchlist };

filters {
    input    => 'chomp',
    expected => 'chomp',

    input_ptr    => [qw{ PTR dumper }],
    expected_ptr => [qw{ split sort array dumper }],

    input_a    => [qw{ A dumper }],
    expected_a => [qw{ split sort array dumper }],

    input_ns    => [qw{ NS dumper }],
    expected_ns => [qw{ split sort array dumper }],

    input_mx    => [qw{ MX dumper }],
    expected_mx => [qw{ split sort array dumper }],
};

sub nameservers {
    $res->nameservers($self