address Logo

Pegasus 2.4, CGI/1.1 and WebDAV

目次

この記事の主たる目的は、筆者が Pegasus 2.3 を Pegasus 2.4 に改訂するに当たって筆者自身の思考を整理することにある。ついでに、Plan 9 や Pegasus を知らない人への簡単な解説もされている。web サーバを作ってみたいと言う人には何か役に立つことがあるかも知れない。

記事の性格上、纏まったものではない。雑多なプログ風の印象を受けるであろう。

間違いがあれば、あるいはより良い提案があればメールを頂ければ幸いである。

WebDAV サーバの設計と実装

筆者の実験環境

OS: Plan 9
host: pc
server: Pegasus 2.4
client: Mac/OSX 10.4, WinXP など

目標

目標は WebDAV を Pegasus の CGI で実装。以下では、この CGI スクリプトを webdav とする。

サーバへは

    http://pc/dav

でアクセスする。"/dav" はデータを置く(ドキュメントルートからの)パスである。

注意

以下の説明の中で "http", "host", "foo", "bar", "alice" などの語がでてくるか、これらは(特に説明がない限り)以下の語を代表している。

http
http, https
host
domain-name
domain-name:port-address
IP-address
IP-address:port-address
foo, bar, baz
バス名。あるいはパスの一部。"/" を含んでよい。"baz" はクエリーを表していることもある。
alice
システムのユーザ名

Pegasus の特徴

名前空間の仮想化

筆者は IWP9(International Workshop for Plan 9)でのプレゼンテーションで Pegasus の特徴を語るに MMU(Memory Management Unit)と比べた。MMU はメモリアドレスを仮想化する、現代のコンピュータに欠かせない基本技術である。これによってプロセスの干渉の問題が解決し、マルチユーザ、マルチタスクの OS が安全に動くようになったのである。

unix などの web サーバの CGI サービスが抱えている基本的な問題は、サービスの相互干渉である。"foo" と "bar" を cgi プログラムとせよ。

    http://host/foo
    http://host/~alice/bar

"foo" も "bar" も同じファイルへアクセスできることが問題が発生する基本的な原因になっている。これは MMU を備えていないコンピュータでマルチユーザ・マルチタスクの OS を実現しようとする時に発生するのとよく似た問題である。この問題の解決にはファイルシステムの名前空間の仮想化技術が必要である。Plan 9 はそれを実現している唯一の OS である。Pegasus はこの技術の下で CGI プログラムの相互干渉問題を解決している唯一の web サーバーである。

Pegasus の下では httpd は仮想的な名前空間を見る。その名前空間の中では、実ホストも仮想ホストも alice のような普通のユーザも共通のドキュメントルート("/doc")を持っている。httpd から見えるドキュメントルートへのパスは同じでも、もちろんパスの実体は異なっている。alice のドキュメントを処理している httpd が、ドキュメントルートは "/doc" であると錯覚していても、実際の alice のドキュメントルートは "/usr/alice/web/doc" なのである。

Pegasus では「 httpd ルート」の概念が本質的である。alice の場合の httpd ルートは "/usr/alice/web" である。実ホストは実ホストの httpd ルートを持っている。仮想ホストも然りである。それらはのパスは "/sys/lib/httpd.rewrite" で決定される。

Pegasus の「 httpd ルート」は通常の httpd サーバーの「 httpd ルート」と異なり、httpd のインストール場所を表さない。それは httpd から見える世界と見えない世界の境界線を意味している。以下では「 httpd ルート」の一つを $web で表す。
従って Pegasus では次の2つの URI

    http://host/foo
    http://host/~alice/foo

に現れる "foo" は、httpd から見れば共に

    /doc/foo

であるが、そのファイル実態は

    $web/doc/foo

であり $web の部分は両者で異なる。つまり 2 つの "foo" は異なったファイルである。

1 つの web サーバの下で複数の利用者が web ドキュメントを作るが、各利用者のドキュメントがサーバの下で奇麗に隔離されている事が Pegasus のセキュリティを他のサーバと別次元のものにしている。さらにサーバの下で名前空間が統一した基本構造を持つ事が、CGI スクリプトの作成を容易にし、1つのスクリプトを変更なしに複数のユーザの間で共有したり、異なるホストの間で流通させる事を可能にする。

CGI/1.1

ここでは CGI/1.1(RFC3875) で規定されている PATH_INFOSCRIPT_NAMEPATH_TRANSLATED について考えてみる。

歴史的に見ると CGI は NCSA の仕様

    http://host/cgi-bin/foo?query

から出発し、Apache では CGI プログラムをどこにでも置けるように

    http://host/path-to-foo/foo?query

のように拡張し、さらに CGI/1.1 では

    http://host/path-to-foo/foo/bar?query

にシンタックスが拡張された。この例では "/bar" は foo によって処理されるファイルへのパスであり、この情報は環境変数 PATH_INFO として foo に渡される。foo はまた URL 上の自分のパス(この例では "/path-to-foo/foo")を知る必要があり、この情報は環境変数 SCRIPT_NAME として foo に渡される。

CGI/1.1 では PATH_TRANSLATED は "/bar" の物理的なバスであるとされている。Pegasus の場合には httpd から見えるバスであり、この値は単に "/doc/bar" とすればよい。ところで "/bar" が何を意味しているかは本来は foo が決める話である。例えば foo が "/doc/baz" より下のファイルだけを処理対象としているならば

    http://host/path-to-foo/foo/bar

の "/bar" は "/doc/baz/bar" であるとして処理を行う必要がある。(そうしないとセキュリティ上の問題に発展する!)
PATH_TRANSLATED には問題に応じた様々な解釈があり得るにも関わらず、これを httpd サーバが勝手に決めようと言うのである。そのために PATH_TRANSLATED の使い道はひどく限られたものにならざるを得ない。CGI/1.2 のドラフトにおいて必須項目から外されたのは当然である。

CGI/1.2 のドラフトは次の URI に載っている。
http://cgi-spec.golux.com/cgi-120-00a.html

Pegasus の CGI Handler

Pegasus の CGI ハンドラ(Pegasus のマニュアルでは Execution handler と書いている) は極めて強力である。CGI/1.1 が行ったようなシンタックス拡張ではなく、機能拡張が設計の出発点になっているからである。

Apache の設定ファイル( httpd.conf など)では CGI ファイルの特徴をサーバに教える仕組みを持っている。例えば拡張子 ".cgi" を持つファイルは CGI ファイルであるとか、指定されたディレクトリのファイルは CGI ファイルであるとかと言う設定である。

Pegasus では、拡張子 ".cgi" の付いた実行ファィルが、CGI ファイルであることをサーバ(Pegasus)に教えるのには "$web/etc/handler" の中に

    *.cgi	text/html   +   $target

の一行を入れることから出発した。

第1フィールドの中の "*" はシェルのワイルドカードのように理解する。但し "*" は "/" にもマッチし、また "/*/" は "/" にもマッチするような修正を加える。
第2フィールドは mime タイプを表しており、第 3 フィールドは CGI ファイルに許されているヘッダの制御のレベルを表しており、 "+" はいわゆる CGI ファイルである事を意味している*。そして第4フィールドの $target は、この場合には問題の CGI ファイルそのものであり Pegasus はこれが実行ファイルであれば、これを実行する。

注: * 初期には第3フィールドには別の意味が与えられていた。

CGI ファイルをこのように表してみると、この表現法は柔軟な拡張性を持つ事にすぐに気づく。

拡張子は自由に設定でき、さらに CGI ファイルの置き場所もきめ細かく設定できる事はすぐに分かるが、そのような事は Apache でも同様に可能である。第4フィールドのあとに、$target に与える引数を指定できるように拡張できる。これは Apache 流ではやれない拡張である。しかし拡張はこのような狭い範囲に留まらない。

第4フィールドに $target ではない、他の実行ファイルを指定すればどうか? 当然それが実行される。そうなると、この拡張は極めて強力なものである事が理解できる。例えば

    /foo/*	-	+	/bin/gate

のような表現を許すのである。(第2フィールドの "-" は mime type が指定されない事を意味しており、この場合には mime type は CGI プログラムが決定するか、あるいはクライアントに送信されるファイルのタイプによって自動的に決定される。) この場合には、ドキュメントルートから見て "/foo/" 以下のパスへのアクセスは丸ごと "/bin/gate" によってトラップされる事になる!

Pegasus のハンドラのアイデアとしては以上の説明で何となく分かったような気がするだろうが、それを実装して行くには乗り越えなくてはならない課題がある。第1フィールドはパターンを表しているが、このパターンと比較されるのは何か? またマッチしたときの $target の値は何か? この問題は簡単そうに見えても、実は意外と難しい問題を含んでいるのである。このテーマに関しては後の議論に回す。(「ハンドラの第1フィールド」の節を見よ。)

この方法の利点は、極めて強力であることの他に、(Apache 流では CGI プログラムの名前が URI の中に露になるのに対して) CGI が使われている事がクライアントからは全く分からない事にある。

Pegasus の handler

    /cgi-bin/foo/*       text/html  +   /doc/cgi-bin/foo

と書けば、Apache などのサーバでは

    http://host/cgi-bin/foo/bar

は CGI プログラム "foo" でトラップされるので、Pegasus の方法は Apache 流を含んでいる事に注意しよう。(もっとも Pegasus ではこのような無粋な CGI の使い方はしないであろう)

Pegasus では CGI/1.1 で要求されている SCRIPT_NAMEPATH_INFO をどのように設定したらよいであろうか? 何しろ CGI/1.1 が想定していないサーバーである。Apache と等価な編成にした場合には互換性を保つ意味で、CGI/1.1 の通りに設定すればよいので考える余地はない。しかしスクリプトの名前が URI の中に現れない場合にはどうか?

Pegasus 2.4 ではこの場合には SCRIPT_NAME は ""(空の文字列)に設定している。従って先の第1フィールドが "/foo/*" の例で、ホストへの

    http://host/foo/bar

の要求では PATH_INFO は "/foo/bar"、PATH_TRANSLATED は "/doc/foo/bar"であり、ユーザ alice への

    http://host/~alice/foo/bar

では PATH_INFO は "/~alice/foo/bar"、PATH_TRANSLATED は "/doc/foo/bar" である。

Pegasus の CGI ハンドラは Plan 9 でなくても実現できると思える。このアイデアは Apache でも採用すればどうでしょうか?

WebDAV のための仕様拡張

X-CGI-Pass

WebDAV をサポートする CGI スクリプトは、(現在の通常のサーバーでは)全てのメソッドの処理をスクリプトが自ら行わなくてはならない。どのサーバーも最低限 GETHEAD メソッドを持っているのであるが、それを活用できない。しかも GET のコーディングは結構大変なのである。GETHEAD のリクエストをサーバーに任せる事はできないであろうか?

このようなニーズは WebDAV のサポートに限らず一般的なものである。そこで今回 Pegasus は CGI ヘッダの拡張仕様 "X-CGI-Pass" を導入する事とした。
rc の言葉で使い方を示すと、

if(~ $method GET HEAD){
        echo 'X-CGI-Pass:'
        echo 'ETag:' $etag
        echo ''
        exit;
}

とするだけで GETHEAD のコーディングが済むことになる。
実はこの仕様は拡張されていて

    X-CGI-Pass: /doc/foo

にすれば、"/doc/foo" を送信してくれる。

ETag として何を採用するかは、プログラムの作成者の好みに任せられている。Pegasus では qid が採用されているが、人に寄っては別の値を採用するであろう。GETHEAD リクエストで使用される ETag の値は PROPFIND リクエストでクライアントに返される ETag の値と整合性を持つ必要があるので、サーバーの処理に任せずスクリプトの値を用いる。

Passwd

2007/01/05

Pegasus のパスワード管理ファイルは Web ドキュメントの管理者ごとに与えられており、そのパスは $web/etc/passwd である。そのファイル 1 つで、基本認証とダイジェスト認証をサポートする。

サーバの私的なファイルにアクセスするユーザの名前を alice とせよ。

WebDAV を使用しない通常の認証では、バス "/foo" へのアクセスに認証を要求する場合には

alice    72e8979b4e26d67fe4920e3fbd2ebffb   /foo alice@hera

ここに "72e8979b4e26d67fe4920e3fbd2ebffb" の部分は

    echo -n alice:alice@hera:blackcat|md5sum

で得られる MD5 ハッシュである。(unix では md5sum ではなく単に md5)。この例ではパスワードは "blackcat"であり、レルムは alice@hera になっている。クライアントはレルムとユーザ名の組が同じであれば( host 名やバスが異なっていても)同じパスワードであると解釈する。(従って本当はレルムにはalice@hera.aichi-u.ac.jp のように世界中でユニークな文字列を指定するのが正しい考え方である。)

この設定は本来ダイジェスト認証用であるが "/sys/lib/httpd.conf" で基本認証を許すように設定すれば、クライアントがダイジェスト認証をサポートしていない時には基本認証が行われる。

WebDAV を使用する時には単に第5フィールドに "*" を付加する。

alice    72e8979b4e26d67fe4920e3fbd2ebffb   /dav alice@hera   *

記号 "*" は実際上は単に "/dev" に対してアクセスメソッドの制限(GET,HEAD,POST,OPTIONS)を外す事を意味している。

なお "/dav" へのアクセスに関しては、いかなるメソッドについても認証が要求される。この仕様*は Windows のクライアントとの関係で問題が発生するかも知れない**

注 * パスワード管理ファイルの中で "*" を添える方法は結局廃止された。詳しくは「Passwd と Handler の仕様再考」を見よ。(2007/01/19)
注 ** WinXP に関しても、この考え方で構わない。(2007/01/18)

CGI スクリプト webdav

2007/01/06

Yuno さんが perl で書いた "webdav.cgi" が存在する。今回はこれを採用する事にした。
基本的には良く出来ていて、たった一行の変更

    - $root = '/home/sugarpot';
    + $root = '/doc';

で、クライアントが Mac/OSX の場合にはマウント、ファイルの一覧表示、ファイルのアップロード、ファイルの消去ができた。(サーバーが unix であればさらにディレクトリの移動やコピーもできたはずである。)

この変更部分はファイルを置くサーバのディレクトリを設定するしているので、サーバ環境に応じて当然変更しなくてはならない性質のものである。サーバが Pegasus の場合の著しい特徴は、Pegasus では $root は "/doc" に固定で構わない。これで実ホストであれ、仮想ホストであれ、それから

    http://host/~alice/dav

のような一般ユーザであれ、$root の変更を必要としない。本来の $root の趣旨に沿った設定は handler で行われる。

webdav.cgi の Plan9 への完全な移植に関しては2つの問題が発生した。perl の rename 命令が unix と Plan9 では動作が違うのである。そのために unix の rename をシミュレートするように書き換えなくてはならなかった。もう一つは webdav.cgi で使用されている flock() の問題がある。flock() は Plan 9 ではサポートされていない。理由は flock() を使うロックのやりかたに不満があるからである。Plan 9 では open()DMEXCL ビットを使う。

Yuno さんの webdav.cgi は WebDAV のメソッドのうち
COPY
PROPPATCH
をサポートしていない。今回 COPY を追加したものの、Mac/OSX クライアントはこのメソッドを活用していない。つまり、サーバ内のコピーも、サーバーとクライアントとの間にファイル内容の転送をしながらサーバー内のコピーを実行する。(将来の改善を期待する)

Yuno さんはかなり真面目に LOCK をサポートしているが、この問題に関しては次に議論する。

LOCK メソッド

2007/01/06

WebDAV では LOCK メソッドのサポートのためにロックファィルを使う。これは普通の意味でのロック(同時書き込みを防ぐロック)ではない。ファイルをエディタで開いた時に、編集中である事を宣言するためのものと考えてよい。この場合には、これにさらにロックを掛けようとすると拒否される。

ファイルのロックを、ロックファイルを使って実装する方法は問題が大きい。クライアントがロックの解除を忘れた場合にはどうなるのか? 当然トラブルの原因になる。しかも十分に起こりうる事態である。(例えばロックをかけたままクライアントがシャットダウンしたとか...) もっともロックの有効時間を設定できるようではあるが...

WebDAV におけるロックの問題点が次の URI でも解説されている。
http://www.atmarkit.co.jp/flinux/special/webdav02/webdav01d.html

WebDAV のパスワードを複数の人間が共有しないものとしよう。この場合にも LOCK 機能は必要であろうか? 必要はないと思える。ロックのもたらす弊害の方が大きいのである。そこで Pegasus への移植に際しては、ロック機能を外す事にした。その方が簡便である。もっとも単にロック機能を外しただけであれば、書き込みができない。そう、不思議な事に RFC2518 には、ファイルの書き込み可能属性をクライアントに伝えるメカニズムは

    <locktype><write/></locktype>

しか見当たらないのだ。

そこでクライアントからはロック機能が存在するかのように見せかけるごまかしを行う。

GET /foo/ の解釈

2007/01/07

ディレクトリへのリクエストの場合 WebDAV は何をすべきか?
GET リクエストの場合に Pegasus はこれまで Pegasus はどのように行ったのかをまず反省する。
以下では "/foo" を何かのディレクトリへの絶対パスとする。リクエスト

    GET /foo HTTP/1.1

を受け取ると、Pegasus はクライアントに

    GET /foo/ HTTP/1.1

を実行してもらうようにリダイレクトし(これをやらないとクライアントが混乱する)、そして、この要求は

    GET /foo/index.html HTTP/1.1

の事であると(クライアントに知らせずに勝手に)解釈し、クライアントに "/foo/index.html" を渡す。(これはどのサーバでも行われているやり方である。)

しかしメソッドか PROPFIND の場合には明らかに

    PROPFIND /foo/ HTTP/1.1

    PROPFIND /foo/index.html HTTP/1.1

と同じ意味ではない。つまり "index.html" を勝手に付加して解釈しても構わないのは GETHEAD だけである。(POST も含めていもよいかも知れないが、POST によるデータを受け取る CGI プログラムは明示的に HTML 文書の中に指定されるのが普通である。)

実際 RFC2518 には次のように書かれている。

The semantics of GET are unchanged when applied to a collection,
since GET is defined as, "retrieve whatever information (in the form
of an entity) is identified by the Request-URI" [RFC2068]. GET when
applied to a collection may return the contents of an "index.html"
resource, a human-readable view of the contents of the collection, or
something else altogether.

サーバの動作を理解するには「実効 path 」(effective path)のような概念が必要であろう。ホストドキュメントへのリクエスト

    GET /foo/ HTTP/1.1

の実効 path は "/foo/index.html" であるが、リクエスト

    PROPFIND /foo/ HTTP/1.1

の実効 path は "/foo/" のままである。

環境変数

Pegasus 2.4 のリリースにあたって、Pegasus の環境変数について反省してみる事にした。

REQUEST_URI

2007/01/08

次のような URI を使ってのクライアントからのアクセス

    http://host/~alice/foo/bar?baz

に対して CGI がこの値を知る事ができればありがたい事がある。例えば WebDAV の PROPFIND などのリクエストの処理の中でも必要になる。

実はこの値は SIP CGI の仕様を定めた RFC3050 で REQUEST_URI にセットするように求められている。すなわち RFC3050 では REQUEST_URI は RFC2396 で言う absoluteURI の事であるとされ、RFC2396 では

    absoluteURI   = scheme ":" ( hier_part | opaque_part )

と書かれている。SIP CGI は HTTP CGI とは同じではないから HTTP CGI が従う必要は無いが参考にはなるであろう。

CGI/1.1 は REQUEST_URI には触れていない。この環境変数は Apache が既に備えていて、先の例の場合には

    /~alice/foo/bar?baz

となるはずである。つまり "http://host" の部分が付かない。REQUEST_URI の言葉の響きからすれば RFC3050 の方が素直であるように思えるが、Request URI の用語は HTTP/1.1 の仕様を定めた RFC2616 の中で

    Request-Line = Method SP Request-URI SP HTTP-Version CRLF

として現れる。つまりリクエストヘッダの中の

    GET /~alice/foo/bar?baz HTTP/1.1

に現れる "/~alice/foo/bar?baz" が Request-URI であり、Apache の REQUEST_URI はこれを踏襲したのであろう。

web サーバが HTTP_SCHEME をサポートしていなければ* SIP CGI の REQUEST_URI (以下の abs_uri )は SERVER_PORTREQUEST_URI を使って、(rc の言葉で表現すれば)

switch($SERVER_PORT){
case 443
        abs_uri=https://$HTTP_HOST$REQUEST_URI
case 80
        abs_uri=http://$HTTP_HOST$REQUEST_URI
case *
        abs_uri=http://$HTTP_HOST:$SERVER_PORT$REQUEST_URI
}

とするしかないであろう。(しかし、これは必ずしも正しくはない)

注 * Apache は HTTP_SCHEME をサポートしていない

ハンドラの第1フィールド

2007/01/12

ハンドラの第1フィールドと比較するのは何か?

リクエスト

    http://host/foo/

の場合

    /foo/

か、それとも

    /foo/index.html

か? 仮に "/foo/" だとすればハンドラの簡明な仕様

    *.html	text/html	0	$target

などが働かなくなる。従って "/foo/index.html" でなくてはならない。

他方、"/foo/index.html" であれば "/foo/" 以下へのリクエストを丸ごと "/bin/gate" にトラップする

    /foo/*	-	+	/bin/gate

のような強力な使い方に問題が発生する。この場合には "/bin/gate" はユーザからのリクエストが

    http://host/foo/

だったのか、それとも

    http://host/foo/index.html

だったのかを判断する必要に迫られることがあるからである。

この問題は実行ファイル "/bin/gate" がリクエストの本来の姿を知る事ができれば解決する。Pegasus 2.4 には環境変数 $target$request があり、$target はどちらの場合にも "/foo/index.html" となる。他方 $request は、前者は "/foo/"、後者は "/foo/index.html" である。

なおスクリプトに渡される環境変数 $target の値はハンドラの中に現れる $target と完全に同じではなく、"/doc" を取り除いたものであり、この例では "/foo/index.html" となる。(このような仕様にしたのは、スクリプトでは "/foo/index.html" に "/doc" をくっつけるのは楽であるが、"/doc/foo/index.html" から "/doc" を取り除くのは面倒だからである。

REQUEST_URI から QUERY を取り除いてしまえば、のこりの部分、いわゆる HTTP/1.1 のパスの部分("/~alice/foo/")は安全に URI デコード( %HH の部分を本来の文字に戻す事)できる。

表1. Pegasus 2.4 の URI に関する環境変数
* request to host document request to user's document decoded? specified by
HTTP URI http://host/foo/?bar http://host/~alice/foo/?bar   HTTP/1.1
$HTTP_SCHEME http http NO Pegasus
$HTTP_HOST host host NO Apache
$REQUEST_URI /foo/?bar /~alice/foo/?bar NO Apache
$REQUEST_USER   alice YES Pegasus
$PATH_INFO / / YES CGI/1.1
$PATH_TRANSLATED /doc/ /doc/ YES CGI/1.1
$SCRIP_NAME /foo /~alice/foo YES CGI/1.1
$QUERY_STRING bar bar NO Apache
$target /foo/index.html /foo/index.html YES Pegasus
$request /foo/ /foo/ YES Pegasus
注釈:
注 *: システムの環境変数 $user は必ずしも $REQUEST_USER に一致しない。(2008/01/18)

WebDAV 特有のリクエストを扱ったときに、ハンドラの第1フィールドを何と比較するか、この問題は今回筆者が最も悩んだ部分である。様々な考え方がある。例えば

    http://host/foo/

において、GETHEADPOST メソッドの時にだけ "index.html" を付加する仕様。あるいは "/foo/" が実際に存在するときにだけ "index.html" を付加する仕様などである。様々な仕様を試した挙げ句、落ち着いた結論は「複雑さは思考を混乱させ、バグの原因になる」である。そして最も単純な仕様(この表の $target )が最終的に選ばれた。

パス名の中の特殊文字

セミコロンの問題

2007/01/12

URI のパス部分に現れるセミコロン(";")は嫌な問題である。

HTTP/1.1 に関する最初の RFC(RFC2068) では

    abs_path       = "/" rel_path
    rel_path       = [ path ] [ ";" params ] [ "?" query ]
    path           = fsegment *( "/" segment )

とされた。ところが RFC2616 では

    http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]]

と改訂された。つまり ";" はパスの構成要素となったのである。この RFC の中では URI のシンタックスは詳しくは書かれていない。単に RFC2396 の参照を促しているだけである。RFC2396 では次のように書かれている。

    path          = [ abs_path | opaque_part ]
    path_segments = segment *( "/" segment )
    segment       = *pchar *( ";" param )
    param         = *pchar

これを見ると ";" はパスの中にいくつ使ってもよいが、何か特殊な意味を持っているらしい事が伺われる。しかし ";" の意味については書かれていない。それでは

    http://host/foo;bar

    http://host/foo%3Bbar

は同じ意味なのか否か? 筆者はこの問題に関する回答を持っていない。

Apache/2.2.3 (Unix) による実験では次のようになっている。

  1. 通情の web ドキュメントへのパスの中では ";" と "%3B" は同じ意味である
  2. PATH_TRANSLATED に渡されるパスの中でも ";" と "%3B" は同じ意味である
  3. CGI ファイル名が ";" を含むと、その CGI へはアクセスできなくなる

最後の点に関しては Apache のバグであろう。Apache は URI のパスの中の ";" に特殊な意味を持たせていないと言える。

Pegasus は RFC2068 に沿って作業を行ったため、現在もそのシンタックスのままであり、 ";" をパスの構成要素としては扱っていない。このことは時にはクライアントとの間に解釈の不一致をもたらすことがある。しかし ";" の意味がはっきりしない以上、軌道修正する価値はない。現時点では「 ";" をパスの文字として使うな」である。

パス名の中の ":"

パス名の中の ":" は %HH でエンコードした方が安全な文字集合の中に含まれている。なぜだろうか? この理由は URI が HTML 文書の中に href によって引用されるケースを考えると理解できる。RFC2396 では(従って HTTP/1.1 では)その場合

    http://host/path
    http:/path
    //host/path
    /path
    path

のいずれの形式も許している(?query#fragment は省略した)。もしも path が "http:/foo" であれば(これは OS のパスとして許される!)、最後の形式は第2の形式と混同される。

同じ問題は HTTP リクエストの Referer ヘッダや Content-Location ヘッダのように absoluteURIrelativeURI の両方を許しているもので発生する。 リダイレクトに使用される Location ヘッダは absoluteURI のみを許しているので、こうした問題は(ブラウザが馬鹿な事をやっていない限り)発生しないであろう。

セキュリティ

認証のタイミング

2007/01/05

セキュリティの観点から言えば、認証によって保護したいディレクトリへのアクセスに対しては、いかなるメソッドであれ認証を要求したいのである。その方がコーデングがシンプルであり、セキュリティホールが発生する可能性が少ない。クライアントが Mac/OSX の場合には、この考えで構わない。

ところがクライアントが Win の場合には、どうやらこの考えが通用しないらしい。筆者の Win クライアントは

    http://pc/dav

に対して、最初に

    OPTIONS / HTTP/1.1

を試している。Win の WebDAV サーバは、この時に手の内を全て明かし、自分が WebDAV のサーバである事をクライアントに知らせる。(Pegasus はこの場合には、GET, HEAD, POST, OPTIONS だけを許されるメソッドとして返答している。) 次に Win のクライアントは

    OPTIONS /dav HTTP/1.1

を試す。この時に Pegasus は認証を要求するが、どうやらこのタイミングではダメらしい*

OPTIONS だけを認証の対象から外すという考えもあるだろうが、筆者は例外を作るのを好まない。例外は思考を混乱させ、コーディングを複雑にし、そして何よりも CGI スクリプトを攻撃にさらす事になる。

注 *: 現在 WinXP クライアントのマウントに成功しているが、このタイミングで構わない。(2007/01/16)
しかし Win2000 クライアントのマウントには認証なしの OPTIONS を許す必要がある。(2007/01/20)

日本語の問題

ブラウザの入力欄における日本語

2007/01/09

次に示すのは Safari の画面である。アドレスの入力欄に注目する。アクセス先は筆者のサーバ(Plan9/Pegasus)である。

iri

図1: Mac/Safari

Safari はアドレス欄の

    http://ar.aichi-u.ac.jp/こんにちは/

を次のように解釈する。

この例ではポートアドレスが指定されていない。この場合には http プロトコルで指定される標準的なポートアドレス(80)が使用される。

クライアントはサーバ ar.aichi-u.ac.jp の 80 番ポートに以下のようなリクエストを書き込む。

GET /%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF/ HTTP/1.1
Accept: */*
Accept-Language: ja-jp
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3
Connection: keep-alive
Host: ar.aichi-u.ac.jp

ここで、サーバー内のロケーション(Apache の REQUEST_URI)が

    /こんにちは/

ではなく

    /%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF/

となっている事に注意する。これは「こんにちは」の文字コードの内部表現を %HH 形式(8bit の 16 進表現)で表現したものである。文字コードには UTF-8 が用いられる。(日本語の仮名・片仮名は UTF-8 では 3 バイト要する。)

アドレス欄に

    http://ar.aichi-u.ac.jp/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF/

と書いても構わないが、誰もこのような書き方を望まないであろう。

Safari は 「/こんにちは/」をそのままサーバに送らないで、%HH 形式に変換してサーバに送っている。これを受け取ったサーバ(Pegasus)は、これを「/こんにちは/」に翻訳してサーバ上のファイルを探す。なぜこのような回りくどい事をするのか? この議論は別の機会に回すとしても、Apache の REQUEST_URI は、この場合には、

    /こんにちは/

なのか、それとも

    /%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF/

なのか? これは実験すれば解決する話しである。筆者のサーバ(OSX/Apache)では

    http://192.168.1.4/cgi-bin/printenv/こんにちは?世界

のアクセスに対しては

DOCUMENT_ROOT="/usr/local/apache2/htdocs"
PATH_INFO="/こんにちは"
PATH_TRANSLATED="/usr/local/apache2/htdocs/こんにちは"
QUERY_STRING="%E4%B8%96%E7%95%8C"
REQUEST_URI="/cgi-bin/printenv/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF?%E4%B8%96%E7%95%8C"
SCRIPT_NAME="/cgi-bin/printenv"
SERVER_SOFTWARE="Apache/2.2.3 (Unix) DAV/2"

を表示する。ただし関心を持たなくてもよい部分は省いてある。

ここに表示される「こんにちは」は UTF-8 でエンコードされている。この事は Mac のファイルシステムの文字コードが UTF-8 であるから当然である。プログラムの文字コードが UTF-8 以外であれば、何か対処の方法があるのかも知れないが、筆者は知らないし、またそのような複雑な世界に身を置きたいとも思わない。(この複雑さは「当面のしのぎ」以上の価値を持たない)

Apache の REQUEST_URI にはサーバが受け取った文字列がそのまま収められている。観察すれば分かるように、この中には QUERY_STRING も含まれている。

現状ではどのブラウザも Safari のように入力欄に日本語を扱えるわけではない。Firefox 1.5.0.7 は未だに URI 入力欄に日本語の表示ができない(%HH エンコードされて表示される)。Microsoft IE はクエリーの日本語が文字化けすることがある。

ファイルやディレクトリの日本語名

2007/01/13

Plan 9 の標準文字コードは UTF-8 である。UTF-8 は Plan 9 の研究から生まれ、そして現在ではインターネットの標準文字コードとなっている。そのことが Pegasus のプログラムコードを非常に簡単にしている。

OSX のファイル名やフォルダ名に「だ」や「ダ」のように、仮名濁点や仮名半濁点が含まれる場合に問題が発生する。OSX ではこれらの文字は 6 バイトでコード化されるが、他のシステムでは 3 バイトでコード化されるからである。(この問題の詳しい解説は例えば筆者の解説 http://ar.aichi-u.ac.jp/iri/#id.3.0.0 を見よ)

da-on-xp

Mac/OSX で作成された「名称未設定フォルダ」
WinXP で見ると「ダ」が文字化けしている
(2007/01/19 追加)

OSX は open(既存ファイルを開くときに使う)や create(新たにファイルを作成するときに使う)で 3 バイトの「だ」を使っても、6 バイトの「だ」を使ってもかまわない。他方 dirstat で取り出されたディレクトリの中のファイル名の一覧は 6 バイトの UTF-8 になっている。

筆者は OSX の方式には賛成しない。不必要な混乱を招くだけであり、このやり方が広い支持を得られるとは思わない。(古い方式がちゃんと機能しているときに、混乱をもたらす新しい方式を誰が敢えて採用するか!?) 他方では Mac/Safari は URI の中の「だ」を 3 バイトでコード化している。つまり Apple は 3 バイトの「だ」がインターネット標準であることを認めているのである。

筆者は dirstat が返すファイル一覧を 3 バイトの「だ」にして欲しいと思う。今から変更しても不都合が発生するとは考えにくい。

到達点と反省

現在はクライアントが Mac/OSX の場合には問題なく動作している*

注*: 現在では他に WinXP,Win2000 の動作が確認されている。
認証に関しては、認証なし、基本認証、ダイジェスト認証のいずれも OK である。
また https との組み合わせでも使える。但し WinXP は http 下の基本認証を拒否する。
(2007/01/23)

dav

図2. Mac/OSX のファインダー

見て分かる通り、Pegasus ではちゃんと日本の文字も表示してくれる。WebDAV には unix の実行ビットの概念が無く、全てのファイルに実行ビットが付いた形で表示される。(図でファイルのアイコンが黒いのがそうである。) 筆者の好みから言えば、実行ビットを外して表示してほしい。

クライアントが WinXP の場合には、クライアントは認証に必要な情報をサーバに送らない。(従って認証されない) この原因は現在のところ筆者には分からない**

現在のところ Pegasus は WebDAV に関してはダイジェスト認証との組み合わせでのみ許している***。これは妥当なニーズの捉え方であろうか?

など多様な利用形態があるのではなかろうか?

注**: この問題は最終的には解決された。(2007/01/23)
注***: この制限は最終的には外された。(2007/01/23)

WebDAV時代のセキュリティ対策
http://www.atmarkit.co.jp/flinux/special/webdav02/webdav01a.html

Mac/OSX の挙動

2007/01/04

サーバのアクセスログを見ていると、Mac/OSX クライアントは奇妙なファイルにアクセスするのに気付く。例えば

    GET /foo/blahblah.blah HTTP/1.1

のリクエストを考える。クライアントは "/foo/blahblah.blah" を受け取ったら次に

    PROPFIND /foo/._blahblah.blah HTTP/1.1

をリクエストする。サーバは当然ながら、このファイルを持っていない。このファイルは何か?

これは Mac/OSX の HFS+ ファイルシステムのリソースフォークの代用である。HFS+ ファイルシステムに関する詳しい解説が
http://www.kernelthread.com/mac/osx/arch_fs.html
に載っている。また WebDAV でリソースフォークをサポートした場合に予想される問題点が
http://mail-archives.apache.org/mod_mbox/jakarta-slide-user/200501.mbox/%3C41DB53A6.9000703@uniscope.jp%3E
で議論されている。

Mac/OSX の設計コンセプトに関しては
http://developer.apple.com/documentation/MacOSX/Conceptual/OSX_Technology_Overview/SystemTechnology/chapter_4_section_2.html
を見よ。

WinXP の不思議

2007/01/02

WinXP クライアントが認証されない。初心者向けのインターフェースである「ネットワークプレイスの追加」からマウントに入ったのでは、原因が捉えにくい。そこで Win の net コマンドを使って様子を調べる:

    net use * http://pc/dav

で、ユーザ名とパスワードを求められるが
「システムエラー67」
「ネットワーク名が見つかりません」
と言われる。サーバが受け取ったヘッダを調べてみると、サーバは認証に必要な情報を受け取っていないことが分かる。

Google で調べてみるとこのテーマを扱った記事の何と多い事か。それらの記事に寄ると、結局ダイジェスト認証を諦めて、基本認証+SSL で我慢しているようである。彼らはサーバとして Apache を採用している。多分他のフリーウェアのサーバも同じ事情なのであろう。このような状態は現在でも続いているのか否かは分からない。

原因に関していろいろ取りざたされているが、そもそも認証に必要な情報がサーバに送られていない事実と照らし合わせると、推測されている多くの可能性(例えば Win-X は複雑なことをやっているためにダイジェストの計算を間違えているのではないかとか)は消えて行く。

追記 2007/01/15

この現象は認証とは関係ないらしい。何しろサーバの認証を外しても全く同じである。クライアントは

    PROPFIND /dav/ HTTP/1.1

の要求を出して、レスポンスを受け取ってから「システムエラー67」を報告する。

追記 2007/01/16 ついに成功

ついに WinXP クライアントがマウントした。この話は独立したページで語ることにする。

ひ弱なクライアント

2007/01/06

WebDAV サーバの実験をしていると、サーバとクライアントの間のプロトコル・ミスマッチがクライアントの動きをおかしくして行く事に気がつく。ひどい時には事実上のクラッシュである。この現象は Mac/OSX でも WinXP でも発生する。

マウントはカーネルが介在しているので、しっかり作ってもらわないと困る。

デバッグ環境

2007/01/08

Plan 9 のターミナルは、制御コードを含めて、全ての文字コードを表示する。この仕様はデバッグの時に威力を発揮する。

telnet コマンドはよけいな事をする。もっとも、この仕様は責められない。CR コードは自動的に送り出してくれないとユーザが悲鳴をあげるであろう。筆者はもっと簡単な通信ツール connect を持っている。いっさいの加工をしないのである。connect はデバッグ用に作ったのではないが、WebDAV サーバのデバッグに威力を発揮している。何しろ、サーバへのデータ送信が CR コードを含めて容易に制御できるのだから。

connect

Plan9 ターミナルの画像

User-Agent など、全てを適当に設定して(偽装して)、サーバの応答を確認できる。特に PUT メソッドのように、送信するコンテンツを持っている場合には本当にありがたい。

このケースでは、受け取ったデータがターミナル・ウィンドウに表示される。データが大きい場合には邪魔になる。ファイル foo に保存したい場合には

    term% connect tcp!pc!http > foo

とすればよい。

connect に関しては
http://plan9.aichi-u.ac.jp/admin/control.html
で解説されている。また置いてある場所も書いてある。(もちろん Plan 9 環境が必要である)

注: 今頃気がついたのであるが、この connect の代わりに使えるツールが既にあった。trampoline がそうである。ツールが目標とする主たる用途も実は同じである。 (2009/02/22)
しかし実際に試してみるに、connect と幾分動作が異なる。connect で捉えられるサーバの応答が trampoline では見えない。仕様の違いと言うよりは trampoline のバグかも知れない。 (2009/02/23)

デバッグと認証

2007/01/09

先に passwd の仕様を説明したが、それによると "/dav" にパスワードを設定するには

alice    72e8979b4e26d67fe4920e3fbd2ebffb   /dav alice@hera   *

のようにする。しかしダイジェスト認証が設定されている状況ではデバッグがままならない。そこでユーザ名を "-" にする、すなわち

-    72e8979b4e26d67fe4920e3fbd2ebffb   /dav alice@hera   *

とすると認証なしに "/dav" にアクセスできるようにしてみた。これは当面のデバッグ用に考えたのであるが、仕様にしても良いのではないか、デバッグのニーズは高いのであるから...と考えている。

OSX は RFC が嫌い?

2007/01/09

不思議な現象に気がついた。RFC 関連の rfc2616.txt のようなファイルを Pegasus の DAV サーバからダウンロードできないのだ。ダウンロード(具体的にはデスクトップへのコピー)を試みると、「コピー中」の表示が何十秒か続いて、あげくの果てに「"rfc2616.txt"内の一部のデータを読み込みまたは書き込みできないため、操作を完了てせきません。(エラーコード -36)」と表示される。そして実際に正しくはコピーされない。

他のファイル、例えば写真などのファイルは問題がない。これらは数メガもあるので、サイズの問題ではなさそうである。RFC 関係のファイルだけに起っている現象である。実に不思議だ。connect を使って OSX からのリクエストをエミュレートしても問題はない。

ログの情報を増やしてやると、時々書き込みエラーを起こしている事が分かった。これはクライアントがデータを受け止められなかった事を意味している。しかし、この現象がなぜ特定のファイルに発生するのかが分からない。

さて、サーバのログを見ているうちに、この現象はクライアントのヘッダ "If-Range" と関係しているらしいと気がついた。クライアントからの

GET /dav/rfc/rfc3986.txt HTTP/1.1
User-Agent: WebDAVFS/1.4.1 (01418000) Darwin/8.8.1 (i386)
Accept: */*
If-Range: Mon, 08 Jan 2007 14:55:18 GMT
Range: bytes=12720-
Connection: keep-alive
Host: pc

の要求に対して Pegasus は

HTTP/1.1 206 Partial Content
Server: Pegasus/2.4 (Plan9)
Date: Tue, 09 Jan 2007 06:14:46 GMT
ETag: 19793-23066-1168268118
Accept-Ranges: bytes
Content-Range: bytes 12720-141810/141811
Last-Modified: Mon, 08 Jan 2007 14:55:18 GMT
Content-Type: application/octet-stream

のヘッダで応答していた。しかしどうやら、このままでは OSX がコンテンツのサイズが分からないらしい。(コンテンツのサイズは Content-Range から計算できるのに...)
RFC2616 には次のように書かれている。

10.2.7 206 Partial Content

The server has fulfilled the partial GET request for the resource.
The request MUST have included a Range header field (section 14.35)
indicating the desired range, and MAY have included an If-Range
header field (section 14.27) to make the request conditional.

The response MUST include the following header fields:

- Either a Content-Range header field (section 14.16) indicating
the range included with this response, or a multipart/byteranges
Content-Type including Content-Range fields for each part. If a
Content-Length header field is present in the response, its
value MUST match the actual number of OCTETs transmitted in the
message-body.

つまり、この場合には Content-Length は無くてもよいのである。(しかし connect による実験では Apache/1.3.33 にせよ Microsoft-IIS/5.0 にせよ、この場合にでも丁寧に Content-Length を付けている。)

Pegasus も応答ヘッダに

    Content-Length: 129091

を追加するようにして、ようやく OSX は RFC を嫌わなくなった。もちろん RFC 関係が問題だったのではなく、RFC 関係のテキストのサイズが大きかったのが問題を引き起こしたものと思える。筆者が持っている他のテキストファイルは小さいので、問題が露にならなかっただけであろう。

Safari が変になった

2007/01/09

あれこれ実験をしている間に、Safari の動作がおかしくなった。

    http://pc

のように、サーバのドキュメントルートへのアクセスに対して、index.html を Safari の中に表示しないで、単にダウンロードするのである。明らかに Safari は送られているコンテンツタイプを "application/octed-stream" と勘違いしている! でも coonect が返すコンテンツタイプはちゃんと "text/html" になっているのである。

この問題は Safari の「キャッシュを空にする」で解決した。どうやら Finder の WebDAV クライアントと Safari が干渉し合っている可能性がある。もしそうだとしたら、ハンドラの設定は

    /dav/*  -  +  /bin/webdav

にした方が無難である。この場合には両者のコンテンツタイプは同じになる。

XML 雑感

2007/01/13

XML が流行である。XML はデータ流通の手段を標準化した点で評価できる。しかしシステム管理にまで XML を適用することには首を傾げる。

システム管理に必要なのはグローバルな視野である。その点で旧来 unix で行われてきたテーブル形式は分かりやすい。このやり方では、テーブルのフィールドの意味はコメントで示し、どのフィールドにどのような値が設定されているか一目で分かる。

unix のシステム管理のファイルは、目的ごとに(多数)存在する。目的ごとに分ける考えは悪くはない。問題なのは、これらのファイルがバラバラの場所に存在し(もっとも unix の初期には "/etc" が管理ファイルの置き場所であった)、グローバルな視野が得られない事にある。この解決策は、すべての管理ファイルの場所を案内し(あるいはリンクによって特定のディレクトリに集中し)、内容を見るための初心者向けの良いツール(筆者にとってはエディタと grep があれば十分ではあるが)を追加することにあろう。

XML は管理ファイルの書式としては頂けないが、Mac/OSX の "NetInfo Manager" (これは NeXT の "NetInfo" の Mac 版である)で採用されているグラフィカルなインターフェースはさらに頂けない。管理データを一カ所に集中すると言う思想には賛成であるが、素人向けのグラフィカルなインターフェースを採用する事によって視野がさらに狭くなる。そしてさらに悪い事には、操作の一つ一つについて図を添えた説明が必要になる。さらに言えば、メールで設定や変更の方法を教えるのに全く不向きである。もっとも "NetInfo Manager" は "NetInfo" と同様に、unix の旧来の管理ファイル形式とのインターフェースを備えている。

初心者がコンピュータを管理する上での困難は、データの追加や変更にあるのではない。本当はデータの意味を理解していないことにある。この本質的な問題に関してグラフィカルなインターフェースは何の役割も果たしていないのである。

OSX におけるボリリューム名

2007/02/04

Mac/OSX で WebDAV サーバーをマウントするには「Finder」→「移動」→「サーバへ接続」で

    http://pc/dav

のように WebDAV 用の口 "dav" を指定する。この時 "dav" が OSX のボリューム名になる。別に "dav" の名前でなくてもよいのであるが、やはり "dav" は筆者の好みである。だから調子に乗って

    http://pc/~arisawa/dav

など、実験のためにいくつもの "dav" をマウントすると、OSX の動作がおかしくなる事に気づいた。どうやら OSX は同じ名前のボリューム名を嫌うらしい。名前が衝突しないように変更して不安定な問題は解消した。

ユーザはサーバの設定である "dav" のような名前を変更できない。ユーザが利用するサーバの中には同じボリューム名のものもあるだろう。そうした事を考えると、マウントの時にボリュームの別名を指定できれば良いのだが...

Pegasus の仕様再考

Passwd と Handler

2007/01/18

WinXP のサポートに成功して、ようやく自分が行ってきた事を冷静に反省する心のゆとりが出てきた。$web/etc/passwd の第5フィールドの "*" は(このようなものがパスワードファイルの中で指定される事に)違和感を覚える仕様である。そのために筆者は将来(忘れた頃に)これがなくても動くようにしようとコードをいじくるに違いない。それを防ぐために、何故ここで指定されているかを整理しておく。将来のために。
たいていの CGI スクリプトはオプティミステックに書かれている。本に書かれているものもそうだし、筆者がこれまでに書いてきたスクリプトも例外ではない。つまり、GETPOST 要求しか来ないものと決めつけている。(もっとも Pegasus の場合には自動的に HEAD も処理できるようには作られている。)
しかし WebDAV のような問題を CGI スクリプトで扱うとすれば、スクリプトはもっと多くのメソッドを扱えなくてはならなくなる。すると次の根本的な疑問に突き当たる。「スクリプトはリクエストのメソッドを吟味しながら処理をしなくてはならないのか?」もしそうだとすれば、これまで作成してきたスクリプトは全て書き直しになるし、またスクリプトの作成は遥かに難しいものになろう。普通のスクリプトはサーバーがメソッドに関してある程度のケアをしてくれていると言う前提に立って書かれているのである。
結局、ケアをしてくれるスクリプトと、ケアをしないスクリプトの種類の違いを何らかの方法でサーバに伝える仕組みを持つ以外に解決策はなさそうに思える。とりあえず passwd の第5フィールドを利用したが、もう一つの考え方としてはハンドラの第3フィールドを利用する方法も良いかも知れない。そこで次の新しい仕様を考えてみる。うまく行けば、この仕様に変更する。

追記 2007/01/19

これでうまく行くようである。

"." で始まるファイルの問題

2007/01/19

"." で始まるファイルは Pegasus では CGI を通じてのみアクセスできるファイルであるとされている。実は "$web/etc/passwd" に第5フィールドを入れようとした背景の1つに、"." で始まるファイルの問題があった。Mac/OSX は "." で始まるファイルへ頻繁にアクセスする。例えば

    .DS_Store

を作るほか、ファイル "foo" が存在すれば必ず

    ._foo

にアクセスしにいく。
以上の他に

    .metadata_never_index
    .Spotlight-V100 HTTP/1.1

に対して PROPFIND でアクセスする。

そこでアクセス制限を WebDAV に限って外してみようとした。その結果、"$web/etc/passwd" での機能拡張が行われたのである。

Pegasus では "." で始まるファイルは何の役割も果たしていない。つまり Apache の ".htaccess" のようなファイルを持たない。Pegasus のアクセス制限は全て "$web/etc" の中のファイルによって行われている。それでは Pegasus では "." で始まるファイルは何に使われる事を想定すれば良いのだろうか?

Pegasus では "." で始まるファイルを使いそうなのは CGI スクリプトである。スクリプトが使用するデータファイルはどこに置いても構わないが、管理上分かり易いのはスクリプトと同じ場所である。そしてドキュメントを管理するユーザにとって大切な事は、"." で始まるファイルはスクリプトによってのみアクセスできると言う明快な特性であろう。

既に述べたように Mac/OSX は "." で始まるファイルに頻繁にアクセスする。その結果、軽快さを失い、OSX での使い心地はひどく悪いものになっている。WebDAV が CGI で実現されている事が、事態をさらに悪くしている。なにしろクライアントがアクセスするごとに perl が起動されることになる*。この時に発生するローディングの負荷が動きをさらに重くする。従って CGI スクリプトが起動される前に、サーバの中でブロックされた方が軽快に動くのではないかと思える。

注 *: Pegasus の WebDAV スクリプトは、現在では Lua に置き換えられている。perl よりも遥かに軽快で早い。(2009/02/22)

"." で始まるファイルは(メソッドによらず)アクセスを一律に禁止した方が、サーバーのコードが簡明であり、また規則の簡明さはセキュリティリスクを少なくする。他方 WebDAV での "." で始まるファイルへのアクセスを禁止した場合には Max/OSX クライアントは何かを失い*、また CGI を使うユーザは WebDAV から全てを行う事はできなくなる。

注 * 筆者の経験では "." ファイルへのアクセスを禁止するとクライアントの動きが不安定になった。(この現象は PROPFIND による情報と、実際のアクセス禁止が整合性を持っていなかった事が原因かも知れない。)

結局妥協の結果として、ハンドラの第3フィールドに "*" を指定した場合には "." で始まるファイルへのアクセス禁止を解くことにした。

OSX の "." ファイルの抑制

2007/01/19

Spotlight indexing を止める

Mac/OSX の Spotlight indexing(索引付け)は WebDAV にとって煩い。Spotlight indexing を止める方法が
http://lists.apple.com/archives/Spotlight-dev/2006/Jun/msg00008.html
に解説されている。そこでは

    defaults write /path_to_backup_volume/.Spotlight-V100/_IndexPolicy Policy -int 3

An easier, but less commonly known route to disabling spotlight is to create a .metadata_never_index at the root of the volume you want to disable indexing for. A .metadata_index_homes_only file will also be respected.

が紹介されている。

.DS_Store の作成を止める

また .DS_Store の作成を止める方法が
http://docs.info.apple.com/article.html?artnum=301711-ja
に解説されている。

._foo 問題

._foofoo のリソースフォークと呼ばれる。
._foo 問題に関しては筆者は解決法を知らない。
このファイルの性格は
http://docs.info.apple.com/article.html?artnum=106510
に解説されている。

パスワード管理

2007/01/25
2007/01/23

Apache の ".htaccess" 雑感

Apache の ".htaccess" は感心しない。ドキュメントルートの下のディレクトリが深くなったときに処理効率が悪くなるのは勿論だが、もっと重要な事は、制御が掛かっている場所や内容が直ちには捕えられないところにある。".htaccess" はホームページにアクセスするクライアントから隠されている他に、ページを管理するユーザにとっても深い茂みの仲に存在するのである。こうした事は管理上の問題をもたらす。

Pegasus のパスワード管理の基本コンセプト

ホームページ管理に関する Pegasus のファイルはホームページを所有するユーザごとに与えられ、その場所は "$web/etc" である。$web の値はホームページの所有者ごとに異なる。Pegasus は、ホームページに関するアクセス制御は、ホームページの所有者の仕事であり、システム管理者の仕事ではないと割り切っている。システム管理者に許されているのは、特定のユーザのホームページ全体を無効にしたり、どこか他のサイトに移動したことをアクセスしたクライアントに案内する事だけである。

今回の見直し --- 同一関門に対する複数のユーザ名の混在 ---

今回は WebDAV をサポートするに際して、パスワード管理ファイル "$web/etc/passwd" の仕様を見直すことにした。これまでの "passwd" の仕様について、筆者は満足していた訳ではなかった。理由はナイーブな直感が必ずしも受け入れられなかったからである。筆者はシステム管理の仕様は直感に忠実でなくてはならないと信じている。そうしないとシステム管理の失敗をもたらす。

具体的に言えば "$web/etc/passwd" に次のように書いたとしよう。

    alice		.... 	/foo realm
    alice@host	....	/foo realm

ここに "host" はホストドメイン名、"...." は MD5 のハッシュ値、"/foo" は "$web/doc" からのパスで、関門(保護されるディレクトリ)を表している。その次の "realm" はレルムの値である。Pegasus が WebDAV をサポートし、Win クライアントのアクセスを許すとすれば、このような書き方をしたくなるのであるが、これまでは(ドキュメントに明記しないまま) 1 つの関門に対して複数のユーザ名が使えなかった。これは直感に反する。

今回の見直し --- 同一関門に対する基本認証とダイジェスト認証の混在 ---

さらに次の問題がある。

    alice		.... 	/foo
    alice		.... 	/foo realm

と書いたとしよう。"realm" が含まれていない行は、Pegasus がダイジェスト認証をサポートしていなかった時代の名残で、基本認証の設定である*。 この様に書いてユーザが期待するのは、「ディレクトリ "/foo" の保護は基本認証とダイジェスト認証のいずれかで行われる」であろう。(基本認証を優先する理由はないので、設定の順序はどうでもよいであろう。)
しかし実際には先の複数のユーザ名の問題と同様に、同じ関門に複数の認証方法を指定できなかった。

注*: 第4フィールドが存在しない場合 "realm" には 保護されているパスの値が代用されている。

今回の見直し --- allowbasic ---

基本認証に関してはもう一つの問題がある。"/sys/lib/httpd.conf" の中でパラメータ "allowbasic" を設定できる。これが 0 であれば基本認証は許されない。デフォルトは「許されない」である。さてデフォルトのまま

    alice		.... 	/foo
    alice		.... 	/bar realm

と書いたときにユーザは何を期待するであろうか? "realm" が含まれる行は基本認証とダイジェスト認証の両方をサポート可能である。

これまでは Pegasus は "allowbasic" が 0 の場合には、いずれの場合にでも基本認証が許されず、"realm" を含まない行は無効になったが、やはりこれも直感に反するであろう。ユーザが "/foo" に基本認証の設定をするのは、そこは基本認証でやりたいからである。従って "allowbasic" は "realm" を含む行に対してのみ作用すべきだと思える*

注*: 反対の見方として、"allowbasic" は "/sys/lib/httpd.conf" で設定されているのだから、システム管理者の指示であると考える事もできる。つまり基本認証の仕様を完全に無効化したい場合の指示と考えるのである(これは Pegasus のこれまでの立場である)。これも理に適っている。
またユーザのパスワード管理の好みは "$web/etc" の中で行うべきであるとの考えもあろう。しかし、その場合には新しい名前の管理ファイルを導入しなくてはならなくなり、たかが "allowbasic" のためだけにそのようなものを導入するのは大袈裟である。
実は筆者のシステムには Pegasus がまだダイジェスト認証をサポートしていなかった時代のユーザがいて、基本認証が使用されている。その生のパスワードは分からない。そのためその設定は、ダイジェスト認証に変更する事無く、そのままになっている。しかし筆者自身が使用するディレクトリには、必ずダイジェスト認証が働くようにしたいのである。特に WebDAV の場合には。
また有力な考え方として "allowbasic" を持ち込むセンスが悪いと言うのもある。パスワードの新しい形式では、基本認証なのか、ダイジェスト認証なのか、あるいは両方なのかを明示的に指定できるようにすべきであるとの考えである。(2007/01/25)

いろいろ考えたあげく、結局 "allowbasic" を廃止し、新形式の場合に第5フィールドを追加し新たに "Basic" と "Digest" のキーワードを許す事にした。

    alice		.... 	/bar realm Digest

は Digest 認証を指定する。同様に "Basic" は基本認証を指定する。指定が無い場合には、クライアントとサーバの交渉に任せる。BasicDigest のキーワードは実際には先頭の1文字だけを見ている。このような実装にしたのはスペルミスを恐れたからである。大文字でも小文字でもよい。(2007/01/26)

NB: 古い形式は将来廃止される。

同一関門、同一ユーザの複数のパスワードは?

1つの関門を同一ユーザ名でパスワード部分(MD5ハッシュ)だけが違う場合はどう扱うか? つまり次の例において "...." の内容が異なる場合の扱いである。(realm はあっても無くてもよい)

    alice		.... 	/bar realm
    alice		.... 	/bar realm

これは論理的に言えば、どのパスワードで入っても構わないと言うことになるが、それはコードをひどく複雑にするし、これを許す事にニーズがあるとも思えない。システム管理の立場から言えば、この場合には1つのバスワードで統一した方が良いのである。そこで、このような書き方は認めない事にしよう。

NB: 特にガードは掛けられていないが、実際には後のパスワードが使用される。

同一関門に対する複数の realm は?

複数のパスワードの問題と似た問題であるが、同一関門に対して複数の realm を設定するのはクライアントとの交渉(negotiation)の場面で混乱をもたらすので許されない。

NB:これも特にガードは掛けられていないが、実際には後の realm が使用される。

関門の途中の関門

なお Pegasus は "$web/etc/passwd" に

    alice	....	/foo		realm
    alice	....	/foo/bar	realm

のように、関門の途中にも関門が存在する場合には最も深い関門だけで認証を求める。深い関門で OK であれば、浅い関門は通してやろうという考えである。例えばこの例では、"/foo/bar/baz" へのアクセス要求があったときには "/foo/bar" についてのみ認証を求める。この仕様は合理的である。(Apache はどうしているか筆者は知らないが多分同じだと思う。その方が検査効率を上げるから。)

WebDAV ソフトウエア --- クライアント編 ---

Goliath

2007/01/24

Mac/OSX クライアントは "._" で始まるファイル(リソースフォーク)に執拗に何回も何回もアクセスする。存在しないと言って拒否され続けているにも関わらずである。ネットの記事では 8 回も! これを止める良い解決策は存在しない*

注*: サーバ側での"404 Not Found" の応答メッセージの中の

    Cache-Control: no-cache

を外す手があるらしい。これは正当な解決策とは言えないが、対象をリソースフォークに限れば誤摩化しの手としては使えるであろう。

そのために Mac/OSX クライアントの動きが重くなる。そこで Goliath が軽快なクライアントとして代用されているらしい。Goliath は次のアドレスから入手できる。
http://www.webdav.org/goliath/
筆者が試してみたところ

もちろん軽い。Win クライアントのウェブフォルダも機能的には Goliath と大差はない。そう思えば Goliath でも我慢できるかも知れない。

以下は Goliath からのリクエストヘッダの例である。

HEAD /dav/semi/._RIMG0006.JPG HTTP/1.1
Content-Type: text/plain; charset="us-ascii"
User-Agent: Goliath/1.0.1 (Macintosh-Carbon; PPC)
Authorization: Basic ===
Host: pc


GET /dav/semi/RIMG0006.JPG HTTP/1.1
Content-Type: text/plain; charset="us-ascii"
User-Agent: Goliath/1.0.1 (Macintosh-Carbon; PPC)
Authorization: Basic ===
Host: pc
NB:

筆者の感想であるが、https の下での安定性にはまだ課題があるようだ。

Cadaver

2007/01/27

Cadaver は ftp のようにして使う WebDAV クライアントである。サーバとのインターフェースは全てコマンドである。そしてコマンドは、ほぼ ftp コマンドそのものである。もちろん非常に軽い。

-bash$ cadaver http://pc/dav
Authentication required for arisawa@hera on server `pc':
Username: arisawa
Password: 
dav:/dav/> help
Available commands: 
 ls         cd         pwd        put        get        mget       mput       
 edit       less       mkcol      cat        delete     rmcol      copy       
 move       lock       unlock     discover   steal      showlocks  version    
 checkin    checkout   uncheckout history    label      propnames  chexec     
 propget    propdel    propset    search     set        open       close      
 echo       quit       unset      lcd        lls        lpwd       logout     
 help       describe   about      
Aliases: rm=delete, mkdir=mkcol, mv=move, cp=copy, more=less, quit=exit=bye
dav:/dav/> 

Cadaver は次のアドレスから手に入る。
http://www.webdav.org/cadaver/

文献

Pegasus に関する参考文献

http://plan9.aichi-u.ac.jp/pegasus/

HTTP/1.1 に関する参考文献

http://www.ietf.org/rfc/rfc2616

CGI/1.1 に関する参考文献

http://cgi-spec.golux.com/

WebDAV に関する参考文献