HTTPのクエリパラメータにコロン(:)を書くのは不正なのか。

の続き。

PHPのparse_url()は、

  • "/abc?a=x&time=09:00&x=y" はパースできるのに、
  • "/abc?a=x&time=09:00" だと失敗する。

相対URIで「動作しない」仕様だかららしいのだが、それはともかく、コロンのパーセントエンコードが必須なのか気になったので調べた。

URIの仕様 RFC 3986


まず、基礎となる URI の仕様 RFC 3986 がある。

RFC 3986 で、クエリに使える文字を定義しているABNFは以下の通り。クエリは?から#または末尾までと定義されている。

query         = *( pchar / "/" / "?" )
pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded   = "%" HEXDIG HEXDIG
sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
              / "*" / "+" / "," / ";" / "="

結構いろいろ使える。ただし、これらの文字を自由に使えるというわけではない。

これとは別に2.2で予約文字というのが定義されている。

reserved    = gen-delims / sub-delims
gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
            / "*" / "+" / "," / ";" / "="

queryはゆるいのに、resesrvedはきびしい。

以下、reservedについて説明している箇所を引用し、自分の理解をコメントする(日本語訳ではない)。「コンポーネント」というのは、スキーム、パス、クエリなどの、URIを構成する部品のこと。

A component's ABNF syntax rule will not use the reserved or gen-delims rule names directly;
reservedシンタックスはコンポーネントのABNFシンタックスでは直接使用されない。


each syntax rule lists the characters allowed within that component (i.e., not delimiting it),
各シンタックスルールは、そのコンポーネントで許可された文字をリストする。


and any of those characters that are also in the reserved set are "reserved" for use as subcomponent delimiters within the component.
コンポーネントで許可されていてreservedにも含まれる文字は、サブコンポーネントのデリミタとして使うため、予約されている。


Only the most common subcomponents are defined by this specification;
もっとも共通のサブコンポーネントだけを、この仕様で定義する。


other subcomponents may be defined by a URI scheme's specification, or
それ以外のサブコンポーネントURIスキームの仕様や、


by the implementation-specific syntax of a URI's dereferencing algorithm,
URI参照解決のアルゴリズム実装のシンタックスによって定義されるだろう。


provided that such subcomponents are delimited by characters in the reserved set allowed within that component.
コンポーネントは、予約文字で区切ってサブコンポーネントにできる。

RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax

要するに、

さらに、以下の説明がある。URIを組み立てるアプリケーションについて。

URI producing applications should percent-encode data octets that correspond to characters in the reserved set
URIを組み立てるアプリケーションは、reservedの文字をパーセントエンコードすべき。


unless these characters are specifically allowed by the URI scheme to represent data in that component.
でも、特別に、URIスキームが許可していれば使ってもよい。

RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax

基本、予約文字はエンコードしなければならない。ただし、httpスキームの仕様で、クエリにコロンやスラッシュを使ってもいいよ、というなら、生のまま使える。

また、URIをパースするアプリケーションについて。

If a reserved character is found in a URI component and no delimiting role is known for that character,
デリミタの役割が知られていない予約文字がコンポーネントに見つかった場合は、


then it must be interpreted as representing the data octet corresponding to that character's encoding in US-ASCII.
ASCIIの該当文字として解釈しなければならない。

RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax

これだと、PHPのparse_uri()がコロン(:)を理由にパースに失敗したらダメだと思う(そういう主張はされてないけど)。

で、次に読むのは http URIスキームの仕様だ、と思って調べたんだけど、そんなのは見つからなかった。httpスキームの仕様は単体で存在しないの?

HTML 4.01


自分は今ウェブサービスを作っているので、HTMLは一切関係ない。

関係無いんだけど、他に該当しそうな仕様が見つからないので、参考になりそうなところを見てみる。

If the method is "get" and the action is an HTTP URI,
メソッドがGETでactionの先がHTTP URIだったら、


the user agent takes the value of action,
actionのURIに、


appends a `?' to it,
?をひっつけて、


then appends the form data set, encoded using the "application/x-www-form-urlencoded" content type.
フォームのデータをapplication/x-www-form-urlencoded でエンコードしてくっつける。


The user agent then traverses the link to this URI.
そんでそのURIにアクセスしろ。


In this scenario, form data are restricted to ASCII codes.
このシナリオで扱えるのはASCIIのデータだけだけど。ズコー

Forms in HTML documents

最後の行はさておき。HTML 4.01 の GET リクエストでは、URI の query に x-www-form-urlencoded を使えと言っている。urlencodedという名前を考えると、当たり前のような気もするが、、これはRFC 3986の http スキームについても言える話なんだろうか。

HTML 4.01における x-www-form-urlencoded の仕様は、以下の通り。RFC 1738、1994年のURL仕様を参照している。

Control names and values are escaped.
名前と値はエスケープする。


Space characters are replaced by `+',
スペースは+に。


and then reserved characters are escaped as described in [RFC1738], section 2.2:
予約文字は RFC1738 2.2 に従ってエスケープする。すなわち、


Non-alphanumeric characters are replaced by `%HH',
英数文字以外は%HHに置換する。 (全部?)


a percent sign and two hexadecimal digits representing the ASCII code of the character.
%と十六進数のASCIIコードで云々。


Line breaks are represented as "CR LF" pairs (i.e., `%0D%0A').
改行はCRLF %0D%0A。


The control names/values are listed in the order they appear in the document.
名前と値は文書の順に並べる。


The name is separated from the value by `=' and
名前と値は=で区切り、


name/value pairs are separated from each other by `&'.
各ペアは&で区切る。

Forms in HTML documents

例の、値をセミコロンで区切る話はべつのところに出てくる。

We recommend that HTTP server implementors, and in particular, CGI implementors support the use of ";" in place of "&" to save authors the trouble of escaping "&" characters in this manner.

Performance, Implementation, and Design Notes

でもセミコロンで区切ったら x-www-form-urlencoded の仕様に適合しなくなるのでは?

RFC 1738 URL仕様 (1994 古い)


HTML 4.01が参照している RFC 1738 の2.2は何て言ってるか。

Octets must be encoded
Octetsは以下の場合にエンコードしなければならない。


if they have no corresponding graphic character within the US-ASCII coded character set,
ASCIIの表示可能な文字でない場合。


if the use of the corresponding character is unsafe, or
その文字が安全でない場合。


if the corresponding character is reserved for some other interpretation within the particular URL scheme.
特定のURLスキームで予約されてる場合。

RFC 1738 - A Gopher URL Format

Unsafeで挙げられてるのは、スペースと

<>"#%{}|\^~[]`

で、

All unsafe characters must always be encoded within a URL.
unsafeな文字は常にエンコードしなければならない。

RFC 1738 - A Gopher URL Format

Reservedで挙げられているのは、

;/?:@=&

で、

Thus, only alphanumerics,
英数、


the special characters "$-_.+!*'(),",
非予約文字($-_.+!*'(),)、


and reserved characters used for their reserved purposes
予約された目的で使われる予約文字だけは、


may be used unencoded within a URL.
エンコードせずに使える。

RFC 1738 - A Gopher URL Format

ということなので、データに予約文字が含まれるならエンコードは必須だろうと思う。

RFC 3986 の「スキームが予約文字の使用を許可できる」という話は、 RFC 1738には出てこない。


application/x-www-from-urlencoded


HTML 4.01 以外の application/x-www-from-urlencoded の仕様。

独立して application/x-www-form-urlencoded を規定する仕様書はまだ存在しません。

application/x-www-form-urlencoded

RFC 1866 (HTML 2.0)以来、HTML5草案まで使われ続けてきた。
トラックバックpingでも、このContent-Type名を使用する。
しかし、x-という問題がある。この改善のため、application/www-form-urlencodedをIANAに登録する提案は以前からなされていたが、HTML5のために再び草案が復活した(I-D[hoehrmann-urlencoded-01] [外部リンク] )。
application/www-form-urlencodedのドラフト仕様では、8ビットであり、符号はUTF-8に固定。このためcharsetパラメーターは不正であるとする。

http://www.wdic.org/w/WDIC/application/x-www-form-urlencoded

2011/03のドラフトを見てみたけど、

URIのqueryコンポーネントで使えるようには見えない。全然エスケープが足りてない。

ここまで調べたことのまとめ。

予約文字(:とか/とか)は基本的にエンコードすべきもの。

  • RFC 1738 は、予約文字を常にエンコードする。
  • RFC 3986 は、スキームが特別に許可するなら生の予約文字をデータ表現に使ってよい。

HTML 4.01はRFC 1738を参照しているので常にエンコードする。

RFC 3986 を採用する場合、 http スキームがクエリをどのように定義しているかは不明。

でもさー


httpのクエリに生のコロンやスラッシュが含まれることで、どんな害があるのか、わからない。

URIの可読性を考えたら、少なくともhttpスキームについては、もっと緩めてもいいように思える。RFC 3986 ならそれが可能なのだし。

ちなみに、Googleはコロン(:)をエンコードしない処理を入れているようだった。Googleでa:bと検索すると、ブラウザのURL欄にはq=a:bと出る。画像検索だとa%3Abになる。

エスケープする文字一覧


全てのASCII記号から、エスケープする文字だけ表示するスクリプト。

#! /usr/bin/env ruby
# -*- coding: utf-8 -*-

ascii = []

# 表示可能なASCII文字全部。空白(32)は無し。
(33..126).each do |i|
  ascii << i.chr
end

puts "* エンコード対象の文字一覧"
puts

# 記号だけ残す。
ascii.reject!{|c| c =~ /[a-zA-Z0-9]/ }
puts "           all: " +  ascii.join

# RFC3986 非予約文字
unreserved = %q{-._~}

# RFC2396 非予約文字
unrsvd2396 = unreserved + %q{!*'()} #'

# RFC1738 非予約文字
unrsvd1738 = %q{-._!*'()$,+} #'

# RFC3936 クエリ文字 %はエンコード形式でしか使えないので除外。
query    = %q{/?:@-._~!$&'()*+,;=} #'

# ECMAScript encodeURI()
encodeuri = %q{-._~:/?#@!$&'()*+,;=} #'

# 非・非予約文字
puts "       RFC3986: " + ascii.map {|c| unreserved.index(c).nil? ? c : ' ' }.join

# RFC2396の非・非予約文字。ECMAScript encodeURIComponent()はこれをエスケープする。
puts "       RFC2396: " + ascii.map {|c| unrsvd2396.index(c).nil? ? c : ' ' }.join

# RFC1738の非・非予約文字。
puts "       RFC1738: " + ascii.map {|c| unrsvd1738.index(c).nil? ? c : ' ' }.join

# ECMA encodeURI()
puts "ECMA encodeURI: " + ascii.map {|c| encodeuri.index(c).nil? ? c : ' ' }.join

# クエリで使えない文字
puts "     not query: " + ascii.map {|c| query.index(c).nil? ? c : ' ' }.join

# Ruby URI::UNSAFE /[^-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]/n より
rubysafe = %q{-_.!~*'();/?:@&=+$,[]} #'
puts "rubyURI.escape: " + ascii.map {|c| rubysafe.index(c).nil? ? c : ' ' }.join

結果は、

* エンコード対象の文字一覧

           all: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
       RFC3986: !"#$%&'()*+,  /:;<=>?@[\]^ `{|}
       RFC2396:  "#$%&    +,  /:;<=>?@[\]^ `{|}
       RFC1738:  "# %&        /:;<=>?@[\]^ `{|}~
ECMA encodeURI:  "  %            < >  [\]^ `{|}
     not query:  "# %            < >  [\]^ `{|}
rubyURI.escape:  "# %            < >   \ ^ `{|}

上から順に、

RubyURI.escape()はECMAScriptのencodeURI()と同じく、URIをまるごとエンコードするためものだと思うんだけど、、[]をエンコードしていない。どこから出てきた仕様?文字セットを自由に設定できるのはいいが、デフォルトの使い道はなさそう。

PHP の $_SERVER['REQUEST_URI'] と parse_url() の予想外な動作について。

REQUEST_URI と HTTP_HOST


PHP のサーバ変数 $_SERVER['REQUEST_URI'] には、ふつうパスとクエリが設定される。

'REQUEST_URI'
ページにアクセスするために指定された URI。例えば、 '/index.html'

PHP: $_SERVER - Manual

ただし、常にパスから始まると保証されているわけではない。以下のように、 GET に続けて絶対 URL を書いたリクエストを送ると、

GET http://localhost/test.php?query=value#fragment HTTP/1.1
Host: localhost

$_SERVER['REQUEST_URI'] の値は、

http://localhost/test.php?query=value#fragment

になる。

ポイント: REQUEST_URI は / から始まるとは限らない。リクエスト次第で http://... からの値を設定できる。

PHP では HTTP コンテキストオプションで request_fulluri を設定すると、こういう(不正な) *1リクエストを送れる。

request_fulluri boolean

TRUE を指定すると、リクエストを生成する際に完全な URI (GET http://www.example.com/path/to/file.html HTTP/1.0) が用いられます。これは標準のリクエストフォーマットではありませんが、 このようなフォーマットを要求するプロキシサーバも存在します。

デフォルトは FALSE です。

PHP: HTTP コンテキストオプション - Manual

よく見るコードで、

$url = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];

こんな風に書いてはいけない。これは、 REQUEST_URI だけでなく HTTP_HOST にも問題がある。

'HTTP_HOST'
現在のリクエストに Host: ヘッダが もしあればその内容。

PHP: $_SERVER - Manual

ということは、

GET http://localhost/test.php?query=value#fragment HTTP/1.0

という Host ヘッダが無い HTTP/1.0 のリクエストを送ると、上の $url の値は、

http://http://localhost/test.php?query=value#fragment

になる。HTTP_HOST は空になり、 REQUEST_URI には http:// から始まる値が設定される。

ポイント: HTTP_HOST は値が存在しないかもしれないし、何でも好きな値を設定されるかもしれない。

Apache だとネームベースのバーチャルホストを設定していなければ、あるいは設定していても、適当な Host ヘッダで PHP をリクエストできる。

parse_url()

$_SERVER['REQUEST_URI'] を parse_url() でパースすれば、ホスト・パス・クエリ・フラグメントに分離できてハッピーだ。

と思ったのだが。

この関数は、指定された URL が有効かどうかを調べるためのもの ではなく、単に URL を上で示した 要素に分解するだけのものです。不完全な URL であっても受け入れられますし、 そのような場合でも parse_url() は可能な限り 正しく解析しようとします。

PHP: parse_url - Manual

注意:
この関数は相対 URL では動作しません。

PHP: parse_url - Manual

これ正確には、「相対URLを渡したらどうなっても知らんよ」という意味だった。

相対URLを渡しても大抵パースできるのだが、たまに予想外な失敗の仕方をする。

<?php

var_dump(parse_url("/abc?a=x&time=09:00&x=y"));

var_dump(parse_url("/abc?a=x&time=09:00"));

この結果は、

array(2) {
  ["path"]=>
  string(4) "/abc"
  ["query"]=>
  string(18) "a=x&time=09:00&x=y"
}
bool(false)

になる。上は成功するけど下は失敗するのですよ。驚きだよ。

ポイント: parse_url()に相対URLを渡しても動くように見えるが、渡してはいけない。

しかも、バグレポートが出てるのに却下してる。

ひどくね?

まとめ

PHP はひどい。

*1:追記 HTTP/1.1でGETの後ろに絶対URL (http://...) を書けました。不正じゃないです。 http://tools.ietf.org/html/rfc2616#section-5.1.2

Perl 互換の正規表現で再帰的に grep して Emacs で開く。

リファクタリングのお共に。

コマンドで開く

emacs $(grep -Prl '正規表現' * | grep -v '\.svn')

Prlっていうのが覚えやすくてよい。ほんとは-Perlって書きたいけど無理だった。

Emacs から開く

基本っぽい機能なのに、今まで使ったことなかった。

  • M-x find-grep-dired …… grepにマッチしたファイルをdiredで表示
  • M-x find-name-dired …… ファイル名をワイルドカードで探してdiredで表示。
  • M-x find-dired …… findコマンドのオプションを指定してdiredで表示。

find-name-diredが使いやすそう。

find-grep-diredで特定のディレクトリを除外する方法は見つからなかった。残念。

TAILQ のソースを読んで C のポインタをマスターする。

正月は TAILQ のソースを読んでいた。普段 C を読み書きしないので、とても勉強になった。ポインタの使い方がわかった(ような気持ちになれた)。

TAILQって?

TAILQ は C のマクロで書かれた双方向リンクリストの実装。

BSD、OSX や glibc に含まれている。

基本的な使い方は以下のページが参考になった。

本記事では OSX の、

  • /usr/include/sys/queue.h

を参照する。glibc のTAILQでも基本は同じ。

めまい

元々は C の勉強のつもりで tmux のソースを見ていて、何気なく queue.h を開いてめまいがした。

例えばこんな感じ。

#define TAILQ_LAST(head, headname)                                      \
        (*(((struct headname *)((head)->tqh_last))->tqh_last))

#define TAILQ_PREV(elm, headname, field)                                \
        (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))

ぜんぜん意味がわからない。

#define TAILQ_ENTRY(type)                                               \
struct {                                                                \
        struct type *tqe_next;  /* next element */                      \
        struct type **tqe_prev; /* address of previous next element */  \
        TRACEBUF                                                        \
}

なんでnextとprevで型が違うのか。こわい。コメントも何が言いたいのかわからない。前の次の要素のアドレス?prevは前の要素ではないのか。

結局、理解できたと思えるまで2日くらいかかった。

リストを作ってアドレスを出力するサンプル

サンプル

#include <stdio.h>
#include <stdlib.h>
#include <sys/queue.h>

/*
双方向リンクリスト /usr/include/sys/queue.h の TAILQ を理解するための、
サンプルです。
*/

/* リスト要素 */
struct list_item {
  /* ポインタがどこを指しているかわかりやすくするため、
     TAILQ_ENTRYの前後にメンバー(num1,num2)を入れる。 */
  int num1;

  /*
   要素をリンクする構造体。
   tqe_nextに次の要素のアドレスが設定される。
   tqe_prevに前の要素のtqe_nextのアドレスが設定される。
  */
  TAILQ_ENTRY(list_item) links;

  /* ポインタがどこを指しているかわかりやすくするため、
     TAILQ_ENTRYの前後にメンバー(num1,num2)を入れる。 */
  int num2;
};
 
/* 
  リストの最初と最後を管理するヘッド構造体を宣言する。
  tqh_firstに最初の要素のアドレスが設定される。
  tqh_lastに最後の要素のtqe_nextを指すアドレスが設定される。
*/
TAILQ_HEAD(list_head, list_item);

/* 渡されたヘッドに要素を追加する。 */
void add_items(struct list_head *head)
{
  int i = 0;
  struct list_item *item;

  for (i = 0; i < 3; i++) {
    item = (struct list_item*)malloc(sizeof(struct list_item));
    item->num1 = i;
    item->num2 = 0;

    /* 末尾に追加する。 */
    TAILQ_INSERT_TAIL(head, item, links);
  }
}

/* ヘッドのアドレスを出力する。 */
void print_head(struct list_head *head)
{
  printf("-- HEAD\n");
  printf("%p: head\n", head);
  printf("%p: head->tqh_first      = %p\n", &(head->tqh_first), head->tqh_first);
  printf("%p: head->tqh_last       = %p\n", &(head->tqh_last), head->tqh_last);
}

/* リストの各要素を出力する。 */
void print_items(struct list_head *head)
{
  struct list_item *item;

  /* headが参照するリストをlinksメンバを使ってループする。要素はitemに入れる。 */
  TAILQ_FOREACH(item, head, links) {
    printf("-- TAILQ_FOREACH %d\n", item->num1);

    /* itemのアドレス */
    printf("%p: item\n", item);

    printf("%p: item->num1           = %d\n", &(item->num1), item->num1);
    printf("%p: item->links.tqe_next = %p\n", &(item->links.tqe_next), item->links.tqe_next);
    printf("%p: item->links.tqe_prev = %p\n", &(item->links.tqe_prev), item->links.tqe_prev);
    printf("%p: item->num2           = %d\n", &(item->num2), item->num2);

    /* 
      以下、TAILQ_PREV がどういう仕組みなのか調べるための出力。

      TAILQ_PREVは、指定した要素の1つ前の要素を得るためのマクロ。
      (*(((struct list_head *)(item->links.tqe_prev))->tqh_last))
      のように展開される。

      item->links.tqe_prev が指す先を
      list_headと見なし(struct list_head * にキャスト)、
      構造体の2番目のメンバ(tqh_last = tqe_prev)が指す先に書かれた
      アドレス(tqe_next or tqh_last)が「1つ前の要素」を指している。
      「2歩戻って1歩進む」

      headまでいくと、tqh_lastによってNULLが返る。
      ループが正順でも逆順でも、最後の要素のtqe_nextが終了条件として機能している。
    */
    struct list_item **prev = item->links.tqe_prev;
    struct list_head *prev_head = (struct list_head *)prev;
    printf("             prev_head            = %p\n", prev_head);
    printf("             prev_head->tqh_last  = %p\n", prev_head->tqh_last);
    printf("            *prev_head->tqh_last  = %p\n", *prev_head->tqh_last);
  }
}

/* リスト用に確保したメモリを解放する。 */
void free_list(struct list_head *head)
{
  printf("-- FREE\n");

  /* 要素を順にリストから外して解放 */
  struct list_item *item;
  while(!TAILQ_EMPTY(head)) {
    item = TAILQ_FIRST(head);
    printf("%p -> free\n", item);
    TAILQ_REMOVE(head, item, links);
    free(item);
  }

  /* ヘッドを解放 */
  printf("%p -> free\n", head);
  free(head);
}

int main(int argc, char *argv[])
{
  /* 
    mallocを使って、リストの要素とアドレスを揃える。表示をわかりやすくするため。
    mallocはヒープに、ローカル変数はスタックに保存される。 
  */
  struct list_head *head = (struct list_head*)malloc(sizeof(struct list_head));
  
  TAILQ_INIT(head);    /* リスト作成 */
  add_items(head);     /* 要素追加 */
  print_head(head);    /* ヘッド出力 */
  print_items(head);   /* 要素出力 */
  free_list(head);     /* メモリ解放 */

  return 0;
}

これを実行すると、

$ gcc test.c
$ ./a.out
-- HEAD
0x100100080: head
0x100100080: head->tqh_first      = 0x100100090
0x100100088: head->tqh_last       = 0x1001000d8
-- TAILQ_FOREACH 0
0x100100090: item
0x100100090: item->num1           = 0
0x100100098: item->links.tqe_next = 0x1001000b0
0x1001000a0: item->links.tqe_prev = 0x100100080
0x1001000a8: item->num2           = 0
             prev_head            = 0x100100080
             prev_head->tqh_last  = 0x1001000d8
            *prev_head->tqh_last  = 0x0
-- TAILQ_FOREACH 1
0x1001000b0: item
0x1001000b0: item->num1           = 1
0x1001000b8: item->links.tqe_next = 0x1001000d0
0x1001000c0: item->links.tqe_prev = 0x100100098
0x1001000c8: item->num2           = 0
             prev_head            = 0x100100098
             prev_head->tqh_last  = 0x100100080
            *prev_head->tqh_last  = 0x100100090
-- TAILQ_FOREACH 2
0x1001000d0: item
0x1001000d0: item->num1           = 2
0x1001000d8: item->links.tqe_next = 0x0
0x1001000e0: item->links.tqe_prev = 0x1001000b8
0x1001000e8: item->num2           = 0
             prev_head            = 0x1001000b8
             prev_head->tqh_last  = 0x100100098
            *prev_head->tqh_last  = 0x1001000b0
-- FREE
0x100100090 -> free
0x1001000b0 -> free
0x1001000d0 -> free
0x100100080 -> free

のように出力される。

左側の0x00000000: が、各構造体のメンバが格納されているアドレス。8バイトずつ、0x100100080 〜 0x1001000ef までを使っている。

num1とnum2はリスト要素のデータ。ポインタがどこを指しているか明確にするため、リンク(links)の前後に配置している。

prev_* は TAILQ_PREV がどのように機能しているか調べるための出力。

ポインタはそれぞれ何を指しているか。

おおまかなイメージは、

の図がわかりやすい。実際にはもう少し複雑にできている。上記のサンプルを実行すると、要素3つのリストが作成され、

のようにリンクされる。

TAILQ_FIRST

簡単なところから。TAILQ_FIRSTは先頭の要素を取得する。

#define TAILQ_FIRST(head)       ((head)->tqh_first)

head構造体のメンバ tqh_first にはリスト先頭要素のアドレスが入っている。tqh_firstの宣言のアスタリスク(*)は1個。* は「この先struct」を意味する。

#define TAILQ_HEAD(name, type)                                          \
struct name {                                                           \
        struct type *tqh_first; /* first element */                     \
        struct type **tqh_last; /* addr of last next element */         \
        TRACEBUF                                                        \
}

ポインタが指している先の構造体のメンバを取得するときは、アロー演算子(->)が使える。

TAILQ_NEXT

指定された要素(elm)の次の要素を取得するマクロ。

#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next)

たとえば上のサンプルコードだと、 マクロ引数のfieldにはlinksが指定されて、

(item)->links.tqe_next

と展開される。

最後の要素item2のtqe_nextにはNULLが設定されている。最後までいくと、TAILQ_NEXTはNULLを返す。

TAILQ_FOREACH

TAILQ_FIRSTとTAILQ_NEXTを使うと、正順のループができる。

#define TAILQ_FOREACH(var, head, field)                                 \
        for ((var) = TAILQ_FIRST((head));                               \
            (var);                                                      \
            (var) = TAILQ_NEXT((var), field))

最後までいくと、TAILQ_NEXTがNULLを返し、ループが終了する。

TAILQ_PREV

PREVは、NEXTよりもずっと複雑になる。

#define TAILQ_PREV(elm, headname, field)                                \
        (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))

以下の図はitem2のTAILQ_PREVを取得する場合。

TAILQ_PREVをサンプルコードの変数名で展開すると、

(*(((struct head *)((item)->links.tqe_prev))->tqh_last))

となり、これを分解すると、

struct list_item **prev = item->links.tqe_prev;
struct list_head *prev_head = (struct list_head *)prev;
*(prev_head->tqh_last)

になる。

まず、item2のtqe_prevを取得する。tqe_prev には、item1のtqe_nextのアドレスが格納されている。

次に、キャストを使って item2 の tqe_prev が list_head を指しているものと見なす。item1 の tqe_prev を tqh_last と呼び換える。

item1の tqh_last (= tqe_prev) には、 item0 の tqe_next のアドレスが格納されている。

最後に、アスタリスク * で item0 の tqe_next に格納されたアドレスを返す。これで item1 の先頭のアドレスが得られる。

item2からitem1を得るために、

  • item2->links.tqe_prev
  • item1->links.tqe_next
  • item1->links.tqe_prev
  • item0->links.tqe_next

と辿っていく。


ポイント:

  • 2個戻って1個進む。
    • 正確には2個戻った先に書かれたアドレスを返す。
    • コード上に、図の青矢印に相当する操作はない。
  • C のキャストは、ポインタが別の型を指していることにできてしまう。
    • links の構造体には名前(タグ名)はついていない。
    • link_head と同じ、ポインタの2個の組なのでキャストしても平気。
    • ポインタ虎の巻〜ポインタの型 char 4個の配列をintで一気に初期化する(良くない)例。
  • struct list_item **tqe_prev;
    • ポインタのポインタ。
    • 「自分がポインタ、次もポインタ、次の次がstruct」。
    • tqe_prevの次の次を辿ると自要素の先頭にたどり着く。
    • /* address of previous next element */ というコメントは、previousのtqe_nextメンバという意味か。ちょっとどうかと思う。

TAILQ_PREV 先頭要素の場合。

item1 の TAILQ_PREV を得る場合も item2 と全く同じ。では、リストの先頭 item0 の前の要素を得ようとするとどうなるか。

tqh_lastの呼び換えは発生せず、そのまま最後の要素 item2 の tqe_next に飛ぶ。tqe_nextにはNULLがセットされている。

ポイント:

  • item2 のtqe_next が、TAILQ_NEXTとTAILQ_PREV両方の末端を示す値として使われている。
    • なんかすごい。

TAILQ_LAST

末尾の要素を得る。

#define TAILQ_PREV(elm, headname, field)                                \
        (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))

#define TAILQ_LAST(head, headname)                                      \
        (*(((struct headname *)((head)->tqh_last)     )->tqh_last))

TAILQ_PREV と TAILQ_LAST は、ほぼ一緒。head->tqh_lastのprevのnextでリスト末尾の先頭アドレスを得る。

TAILQ_INSERT_TAIL

リストの更新処理も1つだけ見てみる。

#define TAILQ_INSERT_TAIL(head, elm, field) do {                        \
        TAILQ_NEXT((elm), field) = NULL;                                \
        (elm)->field.tqe_prev = (head)->tqh_last;                       \
        *(head)->tqh_last = (elm);                                      \
        (head)->tqh_last = &TAILQ_NEXT((elm), field);                   \
        QMD_TRACE_HEAD(head);                                           \
        QMD_TRACE_ELEM(&(elm)->field);                                  \
} while (0)

elmが追加する要素。

do-while(0)は、マクロ展開によりセミコロンが問題を起こすのを避けるため。関数呼び出しのように使える、複数の文からなるマクロを書きたい場合は do-while(0)でラップする。

2行目。elmのtqe_nextをNULLに設定する。
3行目。elmのtqe_prevをheadのtqh_lastに設定する。現在の末尾要素のtqe_nextのアドレス。

4行目。これまでの末尾要素の tqe_next に新しく追加する要素の先頭アドレスを設定。
5行目。headのtqh_lastを、新要素のtqe_nextのアドレスに設定。

残り2行はデバッグ用。

リストが空だった場合は、tqe_prev は head の tqh_first を指す。headの初期化時にtqh_lastがtqh_firstのアドレスに初期化されている。

#define TAILQ_INIT(head) do {                                           \
        TAILQ_FIRST((head)) = NULL;                                     \
        (head)->tqh_last = &TAILQ_FIRST((head));                        \
        QMD_TRACE_HEAD(head);                                           \
} while (0)

#define TAILQ_FIRST(head)       ((head)->tqh_first)

gdb で実行

サンプルプログラムを gdb で実行してアドレスを確認してみる。

$ gcc -g test.c
$ gdb a.out

# 132行目、リストを解放する前にブレークポイントを設定。
(gdb) break 132 

# 実行。
(gdb) run

# headを表示。
(gdb) p head
$1 = (struct list_head *) 0x100100080

# headからtqh_firstのアドレス取得。
(gdb) p $1->tqh_first
$2 = (struct list_item *) 0x100100090

# tqh_firstの中身であるitem0を表示。
(gdb) p *$2
$3 = {
  num1 = 0,
  links = {
    tqe_next = 0x1001000b0,
    tqe_prev = 0x100100080
  },
  num2 = 0
}

# tqh_lastがlist_headを指しているものとして取得。
(gdb) p (struct list_head *)head->tqh_last
$4 = (struct list_head *) 0x1001000d8

# $4をlist_headとして表示。
(gdb) p *$4
$5 = {
  tqh_first = 0x0,
  tqh_last = 0x1001000b8
}

# item2のtqh_last (= tqe_prev)の先の先が、item2自身。
(gdb) p **($4->tqh_last)
$6 = {
  num1 = 2,
  links = {
    tqe_next = 0x0,
    tqe_prev = 0x1001000b8
  },
  num2 = 0
}

# 残りを実行して終了。
(gdb) continue
(gdb) quit

Varnish の設定ファイルを使って Basic 認証を実装する。

Varnish 本体に Basic 認証の機能はない。VCL を使って、Basic 認証を実装することならできる。

原始的な実装。

Authorizationヘッダが一致しなかったら401エラーを出す。

backend apache {
  .host = "127.0.0.1";
  .port = "80";
}

# vcl_recv は、クライアントからリクエストを受け取った時に実行される。
sub vcl_recv {
  set req.backend = apache;

  # Authorization ヘッダが一致しなかったら401エラー。
  if (req.http.Authorization != "Basic dXNlcjpwYXNz" &&
      req.http.Authorization != "Basic dXNlcjI6cGFzczI=" ) {
    error 401;
  }

  return (lookup);
}

# vcl_error はバックエンドもしくは VCL 内部でエラーが発生したら実行される。
sub vcl_error {
  if (obj.status == 401) {
    # WWW-Authenticate ヘッダで Basic 認証を要求する。
    set obj.http.WWW-Authenticate = {"Basic realm="Authorization Required""};
    synthetic {"Error 401 Unauthorized"};
    return(deliver);
  }
}

これで user/pass もしくは user2/pass2 でログインできる。

設定ファイルで認証機能を実装できてしまう。Varnish すごい。

Authorizationヘッダは次のようにして生成する。

$ echo -n "user:pass" | base64
dXNlcjpwYXNz
$ echo -n "user2:pass2" | base64
dXNlcjI6cGFzczI=

Mac なら gbase64 が入ってるかもしれない。

Varnishの認証とバックエンドの認証を分ける。

vcl_error は VCL のエラーと、バックエンドのエラーを区別しない。

バックエンドの 401 エラーをそのまま返したい場合は、内部用に適当なHTTPステータスを設定すると良い。

例えば上の error 401; を error 701; に変えて、

sub vcl_error {
  if (obj.status == 701) {
    set obj.status = 401;
    set obj.response = "Unauthorized";
    set obj.http.WWW-Authenticate = {"Basic realm="Authorization Required""};
    synthetic {"Error 401 Unauthorized"};
    return(deliver);
  }
}

とする。

内部用とはいえ、勝手にステータスコードを増やすのはためらわれるが、、。wiki にも例が載っている。

平文のパスワードをハッシュにするため VMOD をインストールする。

上の設定ファイルには Base64 エンコードされただけの生のパスワードが書かれている。これではセキュリティ上よろしくない。せめてハッシュにしておきたい。

Varnish 3.0 から VMOD というプラグイン機能が使えるようになった。

Authentication Module というのが載っていて、おっと思ったが、リンクが切れている。

Digest module を使って、認証ヘッダをハッシュにすることができた。

コンパイルにはmhashが必要。手元のMacだと、

port installed | grep mhash

で見つかるのだが、dylibをリンクする方法がわからず、、 /usr/local にソースから入れた。

Digest module のインストールは、

./configure VARNISHSRC=/usr/local/src/varnish-3.0.2 VMODDIR=/usr/local/varnish-3.0.2/lib/varnish/vmods 
make
sudo make install

とした。VMODDIR は varnishadm を使い、param.show コマンドで確認できる。

/usr/local/varnish/bin/varnishadm -S ./secret -T :6082 param.show

ドキュメントの生成にrst2manが必要と言われるが、無視した。

Digest モジュールを使って、 Authorization ヘッダをハッシュにする。

# モジュールを読み込む。
import digest;

backend apache {
  .host = "127.0.0.1";
  .port = "80";
}

sub vcl_recv {
  set req.backend = apache;

  # アカウント毎にハッシュを生成するのは無駄なので、ヘッダを一時変数代わりに使う。
  set req.http.X-Varnish-Basic-Auth = digest.hash_sha1("SALT " + req.http.Authorization);

  if (req.http.X-Varnish-Basic-Auth != "07af5b04d92d17b077d86a847cd683ea123503df" &&
      req.http.X-Varnish-Basic-Auth != "264fdc6e41d3526dd282671acb4cc1cd8d670a73") {
    unset req.http.X-Varnish-Basic-Auth;
    error 401;
  }

  unset req.http.X-Varnish-Basic-Auth;
  return (lookup);
}

sub vcl_error {
  if (obj.status == 401) {
    set obj.http.WWW-Authenticate = {"Basic realm="Authorization Required""};
    synthetic {"Error 401 Unauthorized"};
    return(deliver);
  }
}

こんな感じ。

Varnishにはローカル変数がないので、ハッシュを一旦ヘッダに設定し、使い終わったら消している。本当に効率がいいかはわからない。数回ならハッシュを計算したほうが速いかもしれない。

ハッシュの生成は、

#! /bin/bash

SALT="SALT"

PASS=`echo -n "$1" | base64`
echo "Basic $PASS"
echo -n "$SALT Basic $PASS" | sha1sum

で行った。申し訳程度にSALTを設定してみた。

手元だと、hash_md5 はうまく動かなかった。hash_sha1 はうまく動いたが、これで運用してみたわけではないのでご注意を。

その他の方法

認証機能だけを一旦バックエンドに送り、認証に成功したら restart を使ってコンテンツを取得し直す、という例をどこかで見た。けど URL が見つからない、、。

Varnish の hit_for_pass ってなに?

結論

「キャッシュしない」ことをキャッシュするのが hit for pass キャッシュオブジェクト。

同じ URL に、同時にアクセスされたとき、

  • キャッシュが見つからない場合 …… 先頭のリクエストだけをバックエンドに送り、残りはそのレスポンスを待つ。
  • hit for pass キャッシュが見つかった場合 …… 他のリクエストを気にせず、全部バックエンドに送る。

という動作になる。

発端

Varnish を3.0にバージョンアップしたら vcl_fetch のreturn (pass); がエラーになった。どうやら、単純に return (hit_for_pass); に置き換えればいいらしい。

The difference in behaviour of pass in vcl_recv and vcl_fetch confused people, so to make it clearer that they are different, you must now do return(hit_for_pass) when doing a pass in vcl_fetch.

Upgrading from Varnish 2.1 to 3.0 — Varnish version trunk documentation

vcl_recv の pass と、vcl_fetch の pass が紛らわしいから、hit_for_pass に変えたよ、と書いてある。

hit for pass 機能の詳しい説明は、以下にあった。

同時アクセスで、キャッシュが存在しない場合、最初のレスポンスを待つ??

これを読むまで、キャッシュが無ければ単純に全てバックエンドに送られるものだと思っていた。言い換えると、人気の高いページのキャッシュが消えた場合、バックエンドにアクセスが集中するものと思っていた。

どうやら、そうではないらしい。

実験

hit_for_pass の挙動は、実験で簡単に確認できた。

default.vcl。実験用の Varnish 設定。全リクエストのキャッシュを参照し、レスポンスは全てキャッシュしない(= hit for pass キャッシュを作成する)。

backend apache {
  .host = "127.0.0.1";
  .port = "80";
}

sub vcl_recv {
  set req.backend = apache;
  return (lookup);
}

sub vcl_fetch {
  # 3.0用
  return (hit_for_pass);

  # 2.1用
  # return (pass);
}

test.php。バックエンドがリクエストを受けたタイミングを確認するためのPHP。ログを書いてスリープして、ログを書く。

<?php

error_log("TEST 1");
sleep(5);
error_log("TEST 2");

?>OK

で、これを連続して3回リクエストする。

$ curl http://localhost:8080/test.php &
$ curl http://localhost:8080/test.php &
$ curl http://localhost:8080/test.php &

すると、出力されるログは、

[Sun Nov 27 13:06:47 2011] [error] [client 127.0.0.1] TEST 1 ←先頭のリクエスト
[Sun Nov 27 13:06:52 2011] [error] [client 127.0.0.1] TEST 2
[Sun Nov 27 13:06:52 2011] [error] [client 127.0.0.1] TEST 1 ←2番目のリクエスト
[Sun Nov 27 13:06:52 2011] [error] [client 127.0.0.1] TEST 1 ←3番目のリクエスト
[Sun Nov 27 13:06:57 2011] [error] [client 127.0.0.1] TEST 2
[Sun Nov 27 13:06:57 2011] [error] [client 127.0.0.1] TEST 2

で、先頭のリクエストを待っていることがわかる。もう一度、同じURLに3回リクエストすると、今度は、

[Sun Nov 27 13:07:55 2011] [error] [client 127.0.0.1] TEST 1
[Sun Nov 27 13:07:55 2011] [error] [client 127.0.0.1] TEST 1
[Sun Nov 27 13:07:56 2011] [error] [client 127.0.0.1] TEST 1
[Sun Nov 27 13:08:00 2011] [error] [client 127.0.0.1] TEST 2
[Sun Nov 27 13:08:00 2011] [error] [client 127.0.0.1] TEST 2
[Sun Nov 27 13:08:01 2011] [error] [client 127.0.0.1] TEST 2

と、待たずにバックエンドに送られる。hit for pass キャッシュを見ていることがわかる。

Varnish 2.1.3 と 3.0.2 で確認した。


参考まで、Varnish の起動・終了に使ったスクリプト。

start.sh

#! /bin/sh

DIR=/Users/koseki/tmp/varnish
VARNISHD=/usr/local/varnish/sbin/varnishd

$VARNISHD -a :8080 \
          -T localhost:6082 \
          -P $DIR/varnishd.pid \
          -n $DIR/work \
          -f $DIR/default.vcl \
          -S $DIR/secret \
          -s file,$DIR/varnish_storage.bin,10M

stop.sh

#! /bin/sh

DIR=`dirname $0`
cat $DIR/varnishd.pid | xargs kill
rm $DIR/varnishd.pid

右寄りのプログラマを粛正する。

まずこれを10回読もう。

コードが2段階以上右に寄ったら、右足はウンコ踏んでると思いましょう。無闇とエディタの横幅を広げるのも良くない。広いディスプレイは甘え。

つぎこれ。

function foo() {
    if (cond) {
        :
        :
        :
        :
    }
}

こういうのは最低です。下のように書きます。

function foo() {
    if (! cond) return; // ガード節で、さっさと帰らせる。
    :
    (正常処理)
    :
}

無駄にインデントが深くなりません。意図が明確になります。

下のようなコードも良くない。

function foo() {
    if (condA) {
        :
        :
    }
    if (condB) {
        :
        :
    }
}

右に寄りすぎ。メソッドを分割することを考えます。