世の中には「賢く」「使いやすく」「メンテされている」Web Application Framework(以後WAFと略す)がたくさんあって、確実にその流れに乗った方がよいケースがほとんどです。ただ、いわゆる「オレオレ」なWAFを「車輪の再発明」と認識しながらつくると世の中のWAFが内部的にどーなってんのかを理解しやすくなったりします。また、WAFに必要な個別の要素が浮き彫りになるので、何かあった時にどこが問題か?といった切り分けや、少ない機能を提供するだけのサービスをつくる際にオリジナルのWAFで高速化や効率化を図るなどが出来るかもしれません。
本記事では、割りと散々出尽くされている感はありますが、全体で100行程度の非常にシンプルなWAFをつくる過程を紹介してみます。決してオレオレWAFを実践で使うことを推奨しているわけではない。というのを断りとして最初に入れておきます。
最低限のコンポーネント
今回試作したWAFの機能は最低限となっているので以下のパーツを組み合わせる形で実装出来ました。
- Request/Response を処理し、Perlの場合PSGIハンドラを返し、Webアプリサーバとつなぐところ
- リクエストメソッド及びパスを加味し適切なコントローラのアクションへディスパッチするRouter
- Viewのレンダリングメソッドを持ったController
これらに肉付けしていくとより高機能で実用的な要求に耐えうるものが出来るでしょう。
Kirino
オレオレWAFにはアニメのキャラクターの名前をつけるのが「個人的な」風習となっているので、Kirinoと名付けました。以下のモジュールで形成されています。
- Kirino.pm
- Kirino/Controller.pm
- Kirino/Router.pm
例えば、MyAppという名前のWebアプリをKirinoを使ってつくる場合、以下のようなディレクトリ構成例になります。
- lib/MyApp.pm - Kirinoを継承
- lib/MyApp/Controller/Root.pm - Kirino::Controllerを継承
- myapp.psgi - PSGIファイル
- templates/echo.tx - テンプレートファイル
継承ベースでKirinoを使う感じですね。
MyApp
先に使い心地を見てもらうために上記で紹介したMyAppの実装を見てみましょう。フォームに入力した値がサブミットされると表示されるという非常に単純なものです。lib/MyApp.pm
ではルーティングルールをstartup
メソッドの中で記載します。
package MyApp;
use Mouse;
extends qw/Kirino/;
sub startup {
my $self = shift;
$self->router->get('/echo', { controller => 'Root', action => 'echo' });
}
1;
この場合「/echo
というパスにGET
メソッドが飛んできたらRoot
というコントローラークラスのecho
メソッドが発火される」という意味です。今のところGET以外にPOSTにも対応しています。
では、Root
コントローラーを見てみましょう。
package MyApp::Controller::Root;
use Mouse;
extends qw/Kirino::Controller/;
sub echo {
my $self = shift;
my $message = $self->req->param('message');
$self->render('echo.tx', { message => $message });
}
1;
Kirino::Controller
を継承することで$self
にRequestオブジェクトを取得するreq
メソッドが生えているのでそれを利用してクエリーパラメータを取得し、値をレンダリングの際に渡しています。その際使うテンプレートファイルであるecho.tx
はこちら。
<divstyle="text-align:center"><p><b>こんにちは!</b></p><p><: $message :></p><p><formaction="/echo"method="get"><inputtype="text"name="message" /><inputtype="submit"value="送信" /></form></p></div>
<: $message :>
の部分で値を展開しているのが分かります。
Kirinoはこのような使い勝手の最低限のWAFとなっておりまして、多数の問題やら課題があるものの、上記のMyAppは正常に動くし、記述もある程度スッキリ書けますね。
コード
では、そのKirinoのコードをコメントとともにベタっと貼ってみましょう。まずはKirino.pm
から。
package Kirino;
use Mouse;
use Kirino::Router;
use Plack::Request;
use Plack::Util;
use Carp;
our $VERSION = "0.01";
has 'router' => (
is => 'ro',
isa => 'Kirino::Router',
default => sub { Kirino::Router->new }
);
no Mouse;
sub app {
my $self = shift;
sub {
my $env = shift;
$self->startup();
# Routerを使いディスパッチさせる
my ($dest, $captured, $is_not_allowed)
= $self->router->match($env->{REQUEST_METHOD}, $env->{PATH_INFO});
# マッチしなかったら404を返す
return $self->return_404 if !$dest || $is_not_allowed;
# コントローラークラスの動的ロード
my $class = Plack::Util::load_class("Controller::$dest->{controller}", ref $self);
my $method = $dest->{action};
# コントローラークラスをインスタンス化
my $instance = $class->new( req => Plack::Request->new($env) );
# アクションの発火
my $code = $instance->$method();
# レスポンス、この場合は、PSGIに従ったものを返却
return $code;
};
}
sub startup {}
sub return_404 {
return [404, [], ['Not Found']];
}
__PACKAGE__->meta->make_immutable();
Kirino::Router
モジュールはRouter::Boom
というCPANライブラリを活用し、get
及びpost
メソッドを生やしているだけです。
package Kirino::Router;
use Mouse;
extends qw/Router::Boom::Method/;
sub get {
my $self = shift;
$self->add('GET', @_);
}
sub post {
my $self = shift;
$self->add('POST', @_);
}
__PACKAGE__->meta->make_immutable();
また、コントローラーは
- リクエストオブジェクトを扱う
- テンプレートと共にレンダリングをしHTMLを返却する
という機能のみが実装されています。
package Kirino::Controller;
use Mouse;
use Text::Xslate;
use Path::Tiny;
use Encode;
has '_xslate' => (
is => 'rw',
isa => 'Object',
lazy_build => 1,
);
has 'req' => (
is => 'ro',
isa => 'Plack::Request'
);
no Mouse;
sub _build__xslate {
my $tx = Text::Xslate->new();
return $tx;
}
sub render {
my ($self, $filename, $args) = @_;
my ($p, $f, $l) = caller;
my $dir = path($f)->parent(4)->child('templates');
my $html = $self->_xslate->render($dir->child($filename), $args);
return [
200,
[ 'Content-Length' => length $html, 'Content-Type' => 'text/html' ],
[ encode_utf8($html) ]
];
}
__PACKAGE__->meta->make_immutable();
以上3つのファイル、合計100行程度で、簡単なWAFが完成しました。特にエラーハンドリングなどあえてしていない部分が多数あるので、改善の余地はありますが、WAFに必要な最低限の要素が分かってきます。
ちなみに今回はPerlで実装し、以下のCPANモジュールを利用しました。
- Mouse
- Path::Tiny
- Plack
- Router::Boom
- Text::Xslate
ってことで、何度目かわからないほどのWAFの再発明でした。Kirinoについてはは以下のGitHubレポジトリに雑な感じで置いてあるのでご参照下さい。
業務でやる必要は全くないと思いますが、まだオレオレWAFつくったことが無い方は、趣味の時間などでチャンレンジすると何かしら学ぶことがあるかもですね!かくいう僕も今回、まだまだ、学ぶことがありましたよぉ!
以上「Webアプリエンジニア養成読本 Advent Calendar 2014」の17日目のネタでした〜