Apache の sub request と internal redirect

極私的メモ。

通常のリクエスト処理フロー

modules/http/http_core.cap_process_http_connection() 関数 を ap_HOOK_process_connection として登録しており,これが(ap_HOOK_process_connection をオーバーライドしていなければ)通常のリクエスト処理の関数となります。

ap_process_http_connection() の実装について フックから Apache の全体像を追う - daily dayflower の再掲になりますが,

ap_process_http_connection()
{
    /*
     * …… request_rec の準備 ……
     */

    ap_RUN_create_request();        // RUN_ALL

    ap_RUN_post_read_request();     // RUN_ALL

    /* quick_handler が用意されてるならとっとと実行→ fast exit */
    ap_RUN_quick_handler();         // RUN_FIRST

    /* request_rec の整備(r->filename トカ,AAA トカ) */
    ap_process_request_internal();

    /* レスポンスハンドラの呼び出し */
    ap_invoke_handler();

    /* ログ吐き(どんなリクエストでも必ず実行) */
    ap_RUN_log_transaction();       // RUN_ALL
}

おおまかに

  1. request_rec の準備
  2. request_rec の整備 (via ap_process_request_internal())
  3. レスポンスハンドラの実行
  4. ファイナライズ

のような行程を経ることになります。

ちなみに ap_invoke_handler() は下記のような実装です。

ap_invoke_handler()
{
    ap_RUN_insert_filter();         // VOID

    ap_RUN_handler();               // RUN_FIRST
}

(今後頻出する)ap_process_request_internal() は下記のような実装です。

ap_process_request_internal()
{
    ap_RUN_translate_name();        // RUN_FIRST
    ap_RUN_map_to_storage();        // RUN_FIRST
    ap_RUN_header_parser();         // RUN_ALL

    ap_RUN_access_checker();        // RUN_ALL
    ap_RUN_check_user_id();         // RUN_FIRST
    ap_RUN_auth_checker();          // RUN_FIRST

    ap_RUN_type_checker();          // RUN_FIRST
    ap_RUN_fixups();                // RUN_ALL
}

レスポンスを返すことはしませんが,それ以外の request_rec にまつわる諸行(r->filename を決定したりアクセス認可をしたり)をおこなうフェーズです。

サブリクエス

サブリクエストの発行については下記の2つのステップを「基本的に」経ることになります。

  1. サブリクエストの作成(lookup)
  2. サブリクエストの実行(via ap_run_sub_req()

サブリクエストの生成

サブリクエストを生成する関数には ap_sub_req_lookup_uri(), ap_sub_req_lookup_file(), ap_sub_req_lookup_dirent(), ap_sub_req_method_uri() の4種がありますが,基本的な構造はどれも一緒です。

共通部分を抜き出すと下記のようなロジックになっています。

request_rec * ap_sub_req_lookup_hogehoge(r, next_filter)
{
    request_rec *rnew;

    rnew = make_sub_request(r, next_filter);

    ap_parse_uri(rnew, new_uri);

    if (ap_is_recursion_limit_exceeded(r)) {
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    ap_process_request_internal(rnew);

    return rnew;
}

つまり,

  1. make_sub_request() でサブリクエスト用 request_rec 構造体を初期化する
  2. ap_parse_uri()URI のパースをおこなう
  3. ap_is_recursion_limit_exceeded() でサブリクエスト・内部リダイレクト回数が所定よりオーバーしていないか検査する
  4. ap_process_request_internal()request_rec の整備をおこなう

というフローになっています。

なお ap_sub_req_method_uri()ap_sub_req_lookup_uri() の場合,ap_process_request_internal() する前に ap_RUN_quick_handler() してますが,まぁあまり気にしなくてよいでしょう。

make_sub_request() の実装

サブリクエスト用 request_rec 構造体を生成する make_sub_request() in server/request.c の実装(静的関数ですが)は下記のようになっています。

static request_rec *make_sub_request(r, next_filter)
{
    request_rec *rnew;

    /* サブリクエスト用サブプールの作成 */
    apr_pool_create(&rrp, r->pool);

    rnew = apr_pcalloc(rrp, sizeof(request_rec));

    /*
     * …… rnew の整備 ……
     */

    ap_set_sub_req_protocol(rnew, r);

    ap_RUN_create_request(rnew);

    return rnew;
}

サブメモリプールを作成しているところがポイントで,あとは request_rec 構造体を用意して初期化しているだけです。また ap_HOOK_create_request が呼び出されますので,メインリクエストであろうとサブリクエストであろうとリクエスト固有の情報の初期化に使うことができます。

上記で呼び出している ap_set_sub_req_protocol() に限っては server/protocol.c に実装があります。

ap_set_sub_req_protocol(request_rec *rnew, const request_rec *r)
{
    /*
     * …… rnew の雑多な環境整備 ……
     */

    rnew->main = (request_rec *) r;
}

重要なところはとくにないのですが,request_recmain フィールドに生成元の request_rec を設定しているところがちょっとポイントです。

サブリクエストの実行

サブリクエストを実行する ap_run_sub_req() の実装は下記のようになります。

int ap_run_sub_req(r)
{
    if (!(r->filename && r->finfo.filetype)) {
        ap_run_quick_handler(r, 0);
    }

    if (! OK) {
        ap_invoke_handler(r);
    }

    ap_finalize_sub_req_protocol(r);
}

こちらがわで response handler を実行していることがわかります。


以上をまとめると

  • サブリクエストの生成……リクエスト構造体の整備フェーズの hook が呼び出される
  • サブリクエストの実行……レスポンスハンドラが呼び出される

ということになります。

つまり,サブリクエストの出力内容(=レスポンスハンドラの役割)が必要ないのであれば,サブリクエストの生成だけおこなっても問題はありません。じっさい添付モジュールでも,サブリクエストの lookup のみおこない,その結果(r->filename 等)をあれこれするというロジックがいくつかみうけられます。これが一番最初に「2つのステップを『基本的に』経る」と書いた理由です。


さらにまとめると,

  • レスポンスに他のリクエストの処理結果を挿入したい場合,サブリクエストの lookup と ap_sub_req_run() を組み合わせて用いる
  • 他の URI のリソースの情報(実体ファイルやアクセス権など)を知りたい場合,サブリクエストの lookup のみおこなう
  • 他のリソースの取得を試みて成功した場合にそのリソースを返す場合,サブリクエストの lookup をおこなってその結果に ap_internal_fast_redirect() をおこなう(後述します)

のような適用場面があるということになります。

内部リダイレクトについて

サブリクエストの lookup には数多くの API が存在しましたが,内部リダイレクトに用いる API は基本的に ap_internal_redirect()ap_internal_redirect_handler() の2つのみです。それぞれの実装をみていきます。

まず ap_internal_redirect()

ap_internal_redirect(new_uri, r)
{
    request_rec *new = internal_internal_redirect(new_uri, r);

    ap_RUN_quick_handler(new, 0);  /* Not a look-up request */
    if (DECLINED) {
        ap_process_request_internal(new);
        if (OK) {
            ap_invoke_handler(new);
        }
    }

    if (OK) {
        ap_finalize_request_protocol(new);
    }
    else {
        ap_die();
    }
}

のようになっています。

ap_intenal_redirect_handler()

ap_internal_redirect_handler(new_uri, r)
{
    request_rec *new = internal_internal_redirect(new_uri, r);

    if (r->handler)
        ap_set_content_type(new, r->content_type);

    ap_process_request_internal(new);
    if (OK) {
        ap_invoke_handler(new);
        if (! OK) {
            ap_die();
            return;
        }

        ap_finalize_request_protocol(new);
    }
    else {
        ap_die();
    }
}

のようになっています。

細部に違いはあるものの,大まかなフローとしては,

  1. internal_internal_redirect() でリダイレクト用 request_rec の用意
  2. ap_process_request_internal()request_rec の整備
  3. ap_invoke_handler() でレスポンスハンドラの呼び出し

のようになっています。

サブリクエストと異なり,これらの関数でレスポンスハンドラの呼び出しまでおこなっています。

internal_internal_redirect() の実装

両者で使われている,内部リダイレクト用 request_rec 構造体の生成用関数 internal_internal_redirect() in modules/http/http_request.c の実装(静的関数ですが)は下記のようになります。

static request_rec *internal_internal_redirect(new_uri, r)
{
    request_rec *new;

    if (ap_is_recursion_limit_exceeded(r)) {
        ap_die(HTTP_INTERNAL_SERVER_ERROR, r);
        return NULL;
    }

    new = (request_rec *) apr_pcalloc(r->pool, sizeof(request_rec));

    /*
     * …… new の整備 (1) ……
     */

    new->prev = r;
    r->next   = new;

    ap_RUN_create_request(new);

    /*
     * …… new の整備 (2) ……
     */

    ap_RUN_post_read_request(new);

    return new;
}

フローは異なりますが,make_sub_request() と似たような感じです。ただ,ap_is_recursion_limit_exceeded() をこの関数サイドで呼んでいるところと,サブリクエストまわりでは呼ばれていない*1 ap_HOOK_post_read_request フックを呼び出しているところが異なります。

また,

    new->prev = r;
    r->next   = new;

のように,内部リダイレクト用 request_rec 構造体の prev フィールドに,呼び出し元の request_rec を設定し,呼び出し元の next フィールドに内部リダイレクト用 request_rec を設定しています。


ちなみに本筋と関係ないですが,response handler 以前の hook で内部リダイレクト処理をおこなうとおかしな挙動(クライアントからみてレスポンスが終了しない)を示します。これは Apache 1.3 API notes - Apache HTTP Server Version 2.2 にも記述されていますが,実際にわたしも経験しました*2。このように response handler 以前に内部リダイレクトをおこないたい場合,request_rec のどこか(notes とか)にリダイレクト先を保存しておいて,それ用のカスタムレスポンスハンドラを登録しておく,というのが定石になります。mod_rewrite でもそのようになっていました。

おまけ: ap_internal_fast_redirect() について

internal redirect 系で ap_internal_fast_redirect() というのもあるのですが,こいつの実装はこんな感じです。

ap_internal_fast_redirect(rr, r)
{
    /* memory pool の統合 */
    apr_pool_join(r->pool, rr->pool);

    /* サブリクエスト構造体をメインリクエスト構造体にコピー */
    copy rr => r;
}

ここからとくにコピーされたリクエストをどうこう,というのはしていません。なので response handler hook で仕掛けるにはよろしくない感じ。(ap_process_request_internal() で呼び出される)request_rec 整備系 hook から呼び出すべきでしょう。

添付モジュールでの使われどころは,たとえば mod_negotiation で,各言語用のリソースに対して sub request の lookup を行い,(ap_run_sub_req() は行わず)適切なリソースがあったらそいつに ap_internal_fast_redirect() するなど。また mod_dir で,DirectoryIndex の各ファイルに対して sub request の lookup を行い以下略とか。

つまり

  1. sub request を発行する
  2. その sub request がよさげだなと思ったら ap_internal_fast_redirect() でそれをメインリクエストに昇格
  3. あとは通常のリクエスト処理にゆだねる

な手順で使われています。

request_recnext, prev, main

いままでちらほらとでてきましたが request_rec 構造体には next, prev, main というフィールドが存在します。

struct request_rec {
    /* ...... snip snip snip ...... */

    /** Pointer to the redirected request if this is an external redirect */
    request_rec *next;
    /** Pointer to the previous request if this is an internal redirect */
    request_rec *prev;

    /** Pointer to the main request if this is a sub-request
     * (see http_request.h) */
    request_rec *main;

    /* ...... snip snip snip ...... */
};

サブリクエストに関する部分を図示すると以下のようになります。

root じゃなくて main でした。暇ができたら図を直します。

内部リダイレクトに関する部分は以下になります。

わざわざ図示するほどのことでもなかったですが,

  • 自分がサブリクエストとして呼ばれている場合 r->mainNULL 以外になる
  • 自分が内部リダイレクト先として呼ばれている場合 r->prevNULL 以外になる
  • おのおの親の親(や兄弟の兄弟)のように順々にたどっていける
    • これを用いてリクエストのループが発生しているかどうか検出可能

ことがわかります。

*1:どこかで呼んでますかね?きちんとフローを追っていないので見逃しがあるかもしれません。

*2:原因についてはきちんと読み込んでいないのでわかりませんでした。

こんなテンプレートエンジンほしい

  • なぜ、いちいちエスケープを手動で指定しなければいけないのか
  • 文脈によって、自動的にエスケープ手法は決定できるはず

と思ってます。

テンプレートエンジン作りたい - kazuhoのメモ置き場

ですよねー。

で,そのへんを自然に取り込んでいるのが,おもに Python 方面で人気のある XML ベースのテンプレートシステムだと思います。

実際,Genshi のドキュメントにも

The main feature is a template language that is smart about markup: unlike conventional template languages that only deal with bytes and (if you're lucky) characters, Genshi knows the difference between tags, attributes, and actual text nodes, and uses that knowledge to your advantage. For example:

  • Intelligent automatic escaping greatly reduces the risk of opening up your site to cross-site scripting attacks (XSS).
Genshi

なんて書いてあります。


XML ベースのテンプレートエンジンというと,げ,XSL みたいなの?と思いがちですが,

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
      lang="en">
  <div py:if="foo">
    <p>Bar</p>
  </div>
  <ul>
    <li py:for="item in items">${item}</li>
  </ul>
</html>
Documentation/xml-templates.html – Genshi

たとえば Genshi や Kid はこんな感じで直感的に理解可能な構文になっており,XML namespace の機能を使ってるので XML としても「自然」なんです。

んでしばらく前に Perl の Genshi みたいなのってないなぁ暇ができたらつくりたいなぁと思ってたんですが*1,当時は真剣に探してなかった*2ことが判明しました。いま検索してみたら,さっくりと NenshiPetalTemplate::TAL というのがみつかりました*3


ただ XML ベースの「一見直感的」なテンプレート構文にも問題点がありまして。厳格な XML ベースにしてしまうと valid な XML でないと通らないエンジンになってしまいます。たとえば,

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/">
  <p>
    <py:if test="foo"><strong></py:if>
      Hoge ...
    <py:if test="foo"></strong></py:if>
  </p>
</html>

思わずこんなの書くユーザがいると思うのですが,これは valid でないので通らないと思います。

とあんなこんなで,結局プログラマーの自己満足にすぎないのかなと思ったのと,忙しいというのがあって手をつけずにいました。


id:kazuhooku さんはおそらく軽量ウェブフレームワークに同梱できるようなコンパクトなものを指向なさっていると思うのであまり関係ないかと思いますが,便乗して最近(Perl の)テンプレートエンジンについて考えていることをつらつらと書いてみました。

まとめ
  • Genshi みたいな XML 系テンプレートエンジンがほしい
  • できれば libxml2 + C によるライブラリ + 各スクリプト言語へのバインディング
    • ライブラリというよりミドルウェア
    • だって各スクリプト言語ごとに実装をつくるのって,おたがい無駄なことしてるみたいじゃね?
    • 各言語によってテンプレート言語の差異が少ないとデザイナへの訴求力も高くね?
    • パース結果を保持したりシリアライズ&キャッシュしたりできたら,それなりに遅くないものができそうじゃない?
  • 自分でつくろうと思ったけど
    • id:kazuhooku さんみたいなスーパーハカーが作ったらうれしいと思った
      • 方向性が違うならいつか自分で作ってみたいです
        • 老後だったりして
追記

あ,別に XML ベースじゃないとそういうのができないとかそういうわけではなくて,XML ベースにしてそれなりのパーサ(libxml2)を使ってテンプレートエンジンをくむと,エンジン側としてテキストノードとかタグとか属性とかの区別がつきやすい(って上記の Genshi のドキュメントにも書いてありますが)ので楽,というのとただの好みです。

*1:そのせいもあって libxml2 について勉強したこともありました。

*2:なんのことはない CPAN で template っていれて適当に眺めただけだったんで。

*3:Nenshi のドキュメントをみたらなぜか例示が日本語 HTML だったのであれ日本人?とおもったら Rintaro さんが作ってたんでしたか。