天然パーマです。

オレオレ Web Application Framework で学ぶ

世の中には「賢く」「使いやすく」「メンテされている」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はこちら。

 <div style="text-align:center">
  <p><b>こんにちは!</b></p>

  <p><: $message :></p>

  <p>
    <form action="/echo" method="get">
      <input type="text" name="message" />
      <input type="submit" value="送信" />
    </form>
  </p>
</div>

<: $message :> の部分で値を展開しているのが分かります。

MyApp

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日目のネタでした〜