記事

Last Modified:

Plack based WAF "Slack" #Perl #Slack

Perlで独自CMSを作成するためにCatalystやDancerなどのWAFを使っていたのですが、気に入らない部分が多かったのでWAFを自作しました。

https://github.com/robario/p5-Slack(是非Starしてください)

まだまだ荒削りで仕様も変わったりしますが、どうか生暖かい目でお願いします。

本体は極力薄いWAFを目指していて、モデルやビューには手を出しません。 ぶっちゃけ手を出すのはコントローラーだけで、Slackという名の通りゆるふわなWAFです。 将来的に

Howdy, Slack!

# --- app.psgi --- #
package MyApp;
use Slack qw(App Controller);

action hello => sub {
    res->body('Hello, world!');
};

MyApp->new->to_app;

ルーティング

まずSlackの一番の肝であるアクションの定義について。 CatalystとDancerのいいとこ取りなイメージです。

基本

基本の(冗長な)記述方法は以下の通り。

action $name, $pattern, \%methods;

$name

$nameはアクション名で、自由に名前をつけることができます。重複していても構いません。

これらはCatalyst inspireです。

$pattern

$patternPATH_INFOにマッチさせる正規表現または文字列を指定します。省略も可能です。

文字列で指定した場合はqr/$pattern\z/に変換され、省略した場合はqr/$name\z/に変換されます。 これは大抵のWAFの挙動と似てますが、\zが付く点に注意が必要です。余計な文字列が付いたPATH_INFOは受理しません。

なお後述しますが、全ての$patternの前にコントローラー固有のprefixが前置されます。

Sinatraで使われている:name形式は用意してませんが、:name形式は型を指定できないことを踏まえて敢えてできないようにしています。 その代わりnamed captureを利用して(?<username>[[:lower:]][[:lower:]\d]{,7})のように記述できます。これを便宜上『型を指定できる』と言ってます。 この利点は、早い段階でリクエストを不受理できることと、コントローラーでのバリデーションがほぼ不要になる点です。

型とバリデーションについては長くなるので後述します。

\%methods

\%methodsREQUEST_METHODと実行コードの組または単一の関数を指定します。

{
    GET    => sub { ... },
    POST   => sub { ... },
    PUT    => sub { ... },
    DELETE => sub { ... },
}

のように記述します。または単一の関数sub { ... }を指定すると{ GET => sub { ... } }に変換されます。

prefix

アプリケーションのパッケージ名がMyAppで、コントローラーのパッケージ名がMyApp::Fooの場合、prefixは/foo/になります。 prefixを調整するにはsub prefixをオーバーライドするだけです。 例えばいわゆるRootコントローラーには

sub prefix {'/'}

と記述します。Dancer inspireです。

package MyApp::Foo;

action bar => sub { ... };    # GET /foo/bar

action bar => {               # ditto
    GET => sub { ... }
};

action bar => 'bar' => {      # ditto
    GET => sub { ... }
};

action bar => qr/bar\z/ => {    # ditto
    GET => sub { ... }
};

action baz => '' => {           # PUT /foo/baz and DELETE /foo/baz
    PUT    => sub { ... },
    DELETE => sub { ... },
};

アクション

リクエスト毎に上記ルーティングでマッチしたアクションコードsub { ... }が呼び出されます。

基本

アクションコードではreqresというキーワードが使え、resに対して応答をセットします。

action foo => qw{(?<name>[[:alpha:]]\w*)} => sub {
    res->body( 'hello, ' . req->args->{name} );
};

アクションコードを実行した結果、res->bodyが空の場合はビューが呼び出されます。 ビューの呼び出し時にはres->stashがパラメータとして渡されます。

action foo => qw{(?<name>[[:alpha:]]\w*)} => sub {
    res->stash->{name} = req->args->{name};
};
hello, [% name %]

Catalyst inspireですが、Catalystの場合はコンテキストオブジェクト->stashですので注意が必要です。 Slackの場合はレスポンスオブジェクト->stashとなります。 何故ならレスポンスを生成するために使われるから混同しないようにするためです。

req is a Slack::Request

Plack::Requestを継承し、独自のargsargvというアクセサが定義されています。 argsには\%+(\%LAST_PAREN_MATCH)、argvには[$1..$n]が格納されており、ルーティングでの$patternでグルーピングした部分が取得出来ます。

通常のcaptureとnamed captureを混ぜた場合は、おおよそ予想通りの挙動になります。

action foo => qw{(?<name>[[:alpha:]]\w*)/(.+)} => sub {
    ...;
};

このアクションをGET /foo/robario/helloと呼び出すと、argvには[$1,$2]argsには{name=>$+{name}}が格納されます。

argv: [
        'robario',
        'hello'
      ]
args: {
        name => 'robario'
      }

res is a Slack::Response

Plack::Responseを継承し、stashアクセサが追加されています。 stashにはビューに渡すパラメータをセットします。

型とバリデーション TODO: 日付の場合どうするか?うるう年とか。それとも、この項目自体消すか?

たまにコントローラーとモデルでのバリデーションの違いが話題に上がりますが、 私は「コントローラー側では型バリデーション」「モデル側では論理バリデーション」と考えています。

コントローラー側 :: 型バリデーション
ここでいう型は整数型・文字列型とかではなくて、UserName型・PageNumber型などのようなものです。 おのずと正規表現のみでバリデーションできるはずです。たぶん。
モデル側 :: 論理バリデーション
呼び出し元で既に正しい型に変換できているので、有効範囲や存在チェックなどをする。
# イメージ

function Controller::action(page) {
  # ここで字面をチェックする。
  pageが/(0|[1-9]\d*)/にマッチしなければエラー
  Model::search( PageNumber::new(page) )
}

function Model::search(PageNumber page) {
  # 既に正しい型に変換できているわけだから、ここでは字面チェックはしない。nilチェックもしない。
  pageがMAX_PAGEを超えていたらエラー
  データを取得してみた結果、pageに対応するデータが無かったらエラー
}

あいにくPerlは型付けが弱いのでこの理論は中途半端な実装になってしまうんですが、考え方としての話です。