仙石浩明の日記

プログラミングと開発環境

2007年3月19日

オープンソース版 VPN-Warp リレー サーバ (Perl POE を使って実装) hatena_b

Perl の非同期I/Oモジュール POE を使って VPN-Warp relayagent を書いてみました」に 続いて、 同じく POE を使って VPN-Warp リレー サーバも書いてみました。 これで、オープンソースだけを使って VPN-Warp を実現することができます。

今までも、 BIGLOBE の VPN ワープのページから証明書を取得すれば、 月額 525円で VPN-Warp を試してみることはできたわけですが、 ちょっと試してみたい場合など、 有料であることがネックである感は否めませんでした。 特に、 常日頃からオープンソースを使いこなしている方々だと、 ちょっと使ってみたいだけなのにお金を払うのはねぇ、 と思ってしまうのではないでしょうか。 かくいう私も、 無料「お試し版」のサービスやソフトウェアに慣れきってしまっているので、 試しに使ってみようとする場合に、 それが有料だったりすると、 いきなり億劫になってしまう今日このごろだったりします (^^;)。

というわけで、 オープンソース版 VPN-Warp です。 使い方はあまりフレンドリーではありませんが、 なんたって全て公開してしまっているので、 興味あるかたは、 とことんいじってみてはいかがでしょうか。

relayagent.pl と同様、 今回公開する relayserver.pl も SSL 暗号化/復号の機能を含んでいません。 したがってリレー サーバへの https アクセスを stone などを 通して SSL 復号する必要があります。 例えば stone を

stone -z cert=cert.pem -z key=priv.pem \
      localhost:12345 443/ssl &

などと実行しておき、 relayserver.pl を

relayserver.pl 12345

と実行します。これだけで 443番ポートはリレー サーバとして利用できます。 つまり、relayagent とブラウザからの https 接続を受付けると、 リレー サーバが両セッションを中継し 「ブラウザ → リレー サーバ → relayagent → Webサーバ」 という経路で通信できます。

                      リレー            イントラ         イントラ
ブラウザ ─────→ サーバ ←──── relayagent──→ Webサーバ
            https     443番ポート                        80番ポート

見かけは極めて tiny ですが、 通信プロトコルは本物(?)の VPN-Warp と互換性があるので、 「VPN-Warp relayagent フリー ダウンロード」から ダウンロードできる VPN-Warp relayagent を使うこともできます。

そもそも論で言えば、 リレー サーバの役目は単にデータを右から左へ渡すだけなので、 以下に示すようにその中核の部分は極めてシンプルです。 しかしながら、もちろんこれは KLab(株) で運用しているリレー サーバが単純であることを意味しません。 機能がシンプルでも、大量の同時接続 & 大量データを受付ける耐高負荷性能や、 機器の一部に故障が起きてもサービスが影響を受けない高可用性を実現するために、 様々な工夫を盛り込んでいます。

では、relayserver.pl の中身を順に見ていきましょう。

#!/usr/bin/perl
use POE qw(Component::Server::TCP Filter::Stream);
my $Port = shift;
my $PollID;
my $PollHeap;
my $PollBuf;
my $PollHeader;
my %SID;
my %Heap;
my %Buf;
my $NextSID = 0;

POE::Component::Server::TCP->new
    (
     Port => $Port,
     ClientInput => sub {
         my ($heap, $input, $id) = @_[HEAP, ARG0, ARG1];
         if (defined $PollID && $id == $PollID) {
             $PollHeap = $heap;
             $PollBuf .= $input;
             &doPoll;
         } elsif (defined $SID{$id}) {
             my $sid = $SID{$id};
             $Heap{$sid} = $heap;
             $Buf{$sid} .= $input;
             &doSession($sid);
         } elsif ($input =~ m@^GET /KLAB/poll @) {
             if (defined $PollID) {
                 $heap->{client}->
                     put("HTTP/1.1 503 Service Unavailable\r\n\r\n");
                 $heap->{client}->shutdown_output;
                 return;
             }
             $PollID = $id;
             $PollHeap = $heap;
             $PollBuf = $input;
             &doPoll;
         } else {
             $SID{$id} = $NextSID;
             $NextSID = ($NextSID + 1) & 0xFFFF;
             my $sid = $SID{$id};
             $Heap{$sid} = $heap;
             $Buf{$sid} = $input;
             &doSession($sid);
         }
     },
     ClientDisconnected => sub {
         my $heap = $_[HEAP];
         my $id = $heap->{client}->ID;
         if (defined $PollID && $id == $PollID) {
             undef $PollHeap;
             undef $PollBuf;
             undef $PollHeader;
             undef $PollID;
         } elsif (defined $SID{$id}) {
             my $sid = $SID{$id};
             undef $SID{$id};
             undef $Heap{$sid};
             undef $Buf{$sid};
         }
     },
     ClientFilter => POE::Filter::Stream->new(),
    );
POE::Kernel->run;

わずか 70行にも満たないコードですが、 リレー サーバの中核の部分は、ほとんどこれで全てです。 いかに POE (Perl Object Environment) の 記述性が高いか分かりますね。

私は常日頃から プログラマの生産性は、ピンとキリでは 3桁の違いがある と主張しています。 この主張をもう少し詳しく言うと、 その 3桁のうち、プログラマの腕に純粋に依存する部分は 2桁ほどの違いで、 残り 1桁ぶんは解決すべき問題に応じていかに最適な道具を使うかの違い、 ということになります。 最適な道具を使いこなせるもの腕のうち、 ということもできますね。

上記 70行にも満たないコードですが、 実は命令文としてみると、 わずかに 2 つの命令文であることが分かります。 すなわち、

POE::Component::Server::TCP->new(...中略...);
POE::Kernel->run;

ですね。実質「POE::Component::Server::TCP->new(...中略...);」 だけと言ってもいいでしょう。 この命令文は、

POE::Component::Server::TCP->new
    (
     Port => $Port,
     ClientInput => sub {
         ... クライアントから受信したデータの処理 ...
     },
     ClientDisconnected => sub {
         ... クライアントとの接続が切れたときの処理 ...
     },
     ClientFilter => POE::Filter::Stream->new(),
    );

という構造になっています。 つまり、クライアントからデータが送られて来たときに呼び出されるルーチンと、 クライアントとの接続が切れたときに呼び出されるルーチンを指定しておけば、 あとは POE がうまくやってくれる、というわけです。簡単でしょう?

リレー サーバにとって「クライアント」というと、 ブラウザか relayagent になります。 クライアントからの TCP/IPセッション一本一本に対して POE が ID を割り振っていて、 この ID を見ればどの TCP/IPセッションで送られて来たデータか分かります。

Perl の非同期I/Oモジュール POE を使って VPN-Warp relayagent を書いてみました」で 解説したように、 クライアントから送られて来た最初のデータが 「GET /KLAB/poll 」で始まっていれば、 そのクライアントは relayagent ですから、 以下のようにその ID ($id) を $PollID に代入しておきます。

         elsif ($input =~ m@^GET /KLAB/poll @) {
             if (defined $PollID) {
                 $heap->{client}->
                     put("HTTP/1.1 503 Service Unavailable\r\n\r\n");
                 $heap->{client}->shutdown_output;
                 return;
             }
             $PollID = $id;
             $PollHeap = $heap;
             $PollBuf = $input;
             &doPoll;
         }

同じ TCP/IPセッション (つまり $id == $PollID) で 続いて送られてきたデータは、 以下の部分で処理されます。

         if (defined $PollID && $id == $PollID) {
             $PollHeap = $heap;
             $PollBuf .= $input;
             &doPoll;
         }

いずれの場合も、受信したデータはいったん $PollBuf に蓄えた上で、 「doPoll」ルーチンを呼び出します。

一方、ブラウザから送られてきたデータの場合は、 以下のようにセッションID ($SID{$id}) を順に割当てていきます。 「セッション」という単語が何度も出てきてややこしいのですが、 $id が POE が各 TCP/IPセッションに割当てた ID で、 各 TCP/IPセッションそれぞれに、 リレー サーバが 16bit の番号を割当てたのが VPN-Warp で言うところのセッションID ($sid = $SID{$id}) です。

         else {
             $SID{$id} = $NextSID;
             $NextSID = ($NextSID + 1) & 0xFFFF;
             my $sid = $SID{$id};
             $Heap{$sid} = $heap;
             $Buf{$sid} = $input;
             &doSession($sid);
         }

同じ TCP/IPセッション (つまりセッションID $sid が $SID{$id}) を通して 続いて送られてきたデータは、 以下の部分で処理されます。

         elsif (defined $SID{$id}) {
             my $sid = $SID{$id};
             $Heap{$sid} = $heap;
             $Buf{$sid} .= $input;
             &doSession($sid);
         }

いずれの場合も、受信したデータはいったん $Buf{$sid} に蓄えた上で、 「doSession」ルーチンを呼び出します。

つまり、relayagent から受信したデータは doPoll ルーチンで、 ブラウザから受信したデータは doSession ルーチンで、 それぞれ処理する、というわけです。 以下の図に示すように、 リレー サーバの役割は、 relayagent から受信した (ブロック化された) データを、 (ブロックを開梱しつつ) ブラウザへ送信し、 またブラウザから受信したデータを、 ブロック化して relayagent へ送ることですから、 doPoll および doSession が何をするためのルーチンか予想できますよね?

VPN-Warp セッション

まず doPoll を見ていきましょう。

sub doPoll {
    do {
        if (! defined $PollHeader) {
            if ($PollBuf =~ /\r\n\r\n/) {
                $PollHeader = `;
                $PollBuf = ';
                $PollHeap->{client}->put("HTTP/1.1 200 OK\r\n\r\n");
            }
        }
        return unless defined $PollHeader;

リクエストヘッダを全て読み込んでいない場合 (つまり $PollBuff に空行 \r\n\r\n が含まれていない場合) は、ここで終わりです。
$PollBuf に受信データが追加されて、ふたたび doPoll が呼ばれるまで待ちます。

リクエストヘッダを全て読み込んだ場合は、 $PollBuf からリクエストヘッダ部分を削除した上で、 次に進みます。

        my ($sid, $len, $data) = unpack("nna*", $PollBuf);
        return unless defined $sid && defined $len && $len ne "";

ブロック全体を読み込めていない場合は、ここで終わりです。 $PollBuf に受信データが追加されて、ふたたび doPoll が呼ばれるまで待ちます。 「ブロック」というのは VPN-Warp 用語で、 relayagent とリレーサーバとの通信は、 基本的にこの「ブロック」を単位にして行ないます。 ブロックは次のような可変長のデータです。

    ┌───┬───┬───┬───┬───┬─≪─┬───┐
    │セッションID│ データ長  │  可変長データ   │
    └───┴───┴───┴───┴───┴─≫─┴───┘
          2バイト         2バイト      「データ長」バイト

「セッションID」および「データ長」は、ビッグエンディアンです。 つまり上位バイトが先に来ます。 したがって、上記コードによって $sid, $len, $data にそれぞれ 「セッションID」「データ長」「可変長データ」が代入されます。

なお、データ長が 0 ないし負数の場合は、 「可変長データ」の部分は 0 バイトになります。 このような「可変長データ」がないブロックは、 コントロール用のブロックで、 EOF や Error などのイベントを伝えます。

        if ($len > 32767) {
            $len -= 65536;
            $PollBuf = $data;
            if ($len == -1) {
                &doShutdown($sid);
            }
        }

$len == -1 のときは、Error を伝えるコントロール ブロックなので、 「doShutdown」ルーチンを呼び出しています。

        elsif ($len > 0) {
            return unless defined $data && length($data) >= $len;
            ($data, $PollBuf) = unpack "a${len}a*", $data;
            if (defined $Heap{$sid}) {
                $Heap{$sid}->{client}->put($data);
            }
        }

$len > 0 のときは、 $sid で示されるブラウザに対して $data を送信します。 $len == 0 のときは、 EOF を伝えるコントロール ブロックなので、 「doShutdown」ルーチンを呼び出しています。

        else {        # len == 0
            $PollBuf = $data;
            &doShutdown($sid);
        }
    } while ($PollBuf ne "");
}

以上を、$PollBuf が空になるまで続けます。

doShutdown はブラウザとの TCP/IPセッションを shutdown するためのルーチンです。

sub doShutdown {
    my ($sid) = @_;
    if (defined $Heap{$sid}) {
        $Heap{$sid}->{client}->shutdown_input;
    }
}

次に doSession です。

sub doSession {
    my ($sid) = @_;
    if (defined $PollHeap) {
        my $req = $Buf{$sid};
        $Buf{$sid} = "";
        for my $block (unpack "(a2048)*", $req) {
            $PollHeap->{client}->
                put(pack("nna*", $sid, length($block), $block));
        }
    }
}

ブラウザから送られてきたデータを、 2048 バイトずつ区切って「セッションID」「データ長」を 前につけることによってブロック化して、 relayagent に送信しています。

オリジナルの VPN-Warp を使ったことがある方は既にお気付きかも知れませんが、 上記 relayserver.pl は説明を簡単にするために機能をいくつか省いています。 例えば、オリジナルのリレー サーバは、 接続する際はクライアント認証が必須で、 同じクライアント証明書を提示した relayagent とブラウザを 結び付ける機能があるのですが、 上記 relayserver.pl はクライアント認証を行なわないので、 任意のブラウザから接続可能ですし、 同時接続が可能な relayagent は一つだけです。

腕に覚えのあるかたは、 オリジナルの VPN-Warp と同等の機能を実現するには どのような修正を加えればよいか、 考えてみてはいかがでしょうか? そして、 こういうことを考えることが好きなかた、 「いっしょにDSASつくりませんか?

Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 06:36
2007年1月22日

Perl の非同期I/Oモジュール POE を使って VPN-Warp relayagent を書いてみました hatena_b

多数の TCP/IP セッションを同時に維持する必要性などから、 非同期I/O が最近流行りのようです。 何をいまさら、という気もするのですが、 いわゆる「最新技術」の多くが 30年前の技術の焼き直しに過ぎない今日このごろなので、 非同期I/O 技術が「再発見」されるのも、 「歴史は繰り返す」の一環なのでしょう。 スレッドが当たり前の時代になってからコンピュータ技術を学んだ人にとっては、 (古めかしい) 非同期I/O が新鮮に映るのかも知れず、 なんだか「ファッションのリバイバル」に似ていますね。

Perl で非同期I/O 処理を手軽に行なうための枠組みとして、 POE: Perl Object Environment というものが あるようです。 POE を使うと、 あたかもスレッドを使っているような手軽さでプログラミングできます。 試しに VPN-Warp の relayagent を POE を使って書いてみました。 オリジナルの relayagent は C 言語で記述した 4000 行を超える プログラムなのですが、 Perl だと 200 行以下で一通り動くものが書けてしまいました (もちろん C 版の機能を全て実装したわけではありません)。

POE を触るのは今回が初めてだったので、 マニュアルをいちいち参照しながら書いたのですが、 なにせわずか 200 行ですから、 開発はデバッグ込みで 1 日かかりませんでした。 改めて Perl の記述性の良さと開発効率の高さに感動したのですが、 これだけ簡潔に書けてしまうと、 relayagent の機能を解説するときの教材としても使えそうです。

というわけで、 今までブラックボックスだった relayagent の中身の解説を試みたいと思います。 これから POE を使ってみようとする人の参考にもなれば幸いです。

VPN-Warp の relayagent とは、 以下の図のようにリレーサーバと Webサーバの両方へ接続して、 リレーサーバから受取ったリクエストを Webサーバへ中継するプログラムです。 http リクエストを受取ってサービスを行なうのですから、 サーバの一種と言えますが、 外部から接続を受付けるわけではなく、 リレーサーバと Webサーバの両方に対してクライアントとして振る舞う点が ユニークと言えるでしょう。

                      リレー            イントラ         イントラ
ブラウザ ─────→ サーバ ←──── relayagent──→ Webサーバ
            https     443番ポート                        80番ポート

http リクエストを受取って Webサーバへ中継するプログラムというと、 proxy サーバを思い浮かべるかも知れません。 proxy サーバはその名の通り、 ブラウザに対してはサーバとして振る舞います:

                                        proxy            イントラ
ブラウザ ──────────────→ サーバ────→ Webサーバ
                                        8080番ポート     80番ポート

proxy サーバが、ブラウザからの接続を受付けて、 それを Webサーバに中継するのに対し、 relayagent は自身では接続を受付けずに中継する、 という違いがお分かりでしょうか? relayagent は接続を受ける必要がないため、 ファイアウォールの内側など、 外部からアクセスできない場所で使うことが可能になっています。

なお、C 版の relayagent はリレーサーバに対して https で接続するのですが、 Perl 版 relayagent (以下 relayagent.pl) は、 説明の都合上 SSL 暗号化の機能を含んでいません。 実際に使うときは、 stone などで SSL 暗号化して リレーサーバに接続する必要があります。

         リレー                         イントラ         イントラ
         サーバ ←──── stone ←── relayagent──→ Webサーバ
         443番ポート       SSL化        Perl 版          80番ポート

例えば stone を

stone -q pfx=relay,5000005.pfx \
      -q passfile=relay,5000005-pass.txt \
      warp.klab.org:443/ssl localhost:12345 &

などと実行しておき、 relayagent.pl はリレーサーバに接続する代わりに、 localhost の 12345 番に接続します。

では、relayagent.pl を順に見ていきましょう。

#!/usr/bin/perl
use POE qw(Component::Client::TCP Filter::Stream);
my $IdleTimerMax = 6;        # 60 sec
&help unless @ARGV == 2;
&help unless shift =~ m/^(\w+):(\d+)$/;
my ($RelayHost, $RelayPort) = ($1, $2);
&help unless shift =~ m/^(\w+):(\d+)$/;
my ($WebHost, $WebPort) = ($1, $2);
my %WebHeap;
my $PollBuf;
my $PollHeap;
my $PollHeader;
my $IdleTimer;
my $DisconectTime = 0;

$RelayHost, $RelayPort は、 リレーサーバのホスト名とポート番号ですが、
前述したように stone 経由でリレーサーバにつなぐために、
$RelayHost = "localhost", $RelayPort = 12345 などとなります。また、 $WebHost, $WebPort は、 中継先となる (イントラの) Webサーバのホスト名とポート番号です。

続いて、リレーサーバへ接続する (直接の接続先は SSL 化を行なう stone ですが、 煩雑になるので以下 「リレーサーバ」 と略記します) ためのコードです:

POE::Component::Client::TCP->new
    ( RemoteAddress => $RelayHost,
      RemotePort    => $RelayPort,
      Connected     => sub {
          $PollHeap = $_[HEAP];
          undef $PollHeader;
          $PollBuf = "";
          $IdleTimer = $IdleTimerMax;
          $PollHeap->{server}->
              put("GET /KLAB/poll HTTP/1.1\r\nX-Ver: realyagent.pl 0.01\r\n\r\n");
      },
      ServerInput   => sub {
          $PollHeap = $_[HEAP];
          $PollBuf .= $_[ARG0];
          &doPoll;
      },
      Filter        => POE::Filter::Stream->new(),
      Disconnected  => \&reconnectPoll,
    );

POE では、非同期に動く処理を、 処理ごとに分けて書くことができます。 各処理のことを「POEセッション」と呼びます。

上記は、リレーサーバへ接続する POEセッションの生成です。 接続先ホストおよびポートを、 それぞれ $RelayHost と $RelayPort に設定しています。

「Connected => sub {」から始まる部分が、 接続に成功したときに実行するコードです。 細かいところはさておき、 接続したら以下のリクエストをリレーサーバに送る、 という点は読み取れるのではないでしょうか。

GET /KLAB/poll HTTP/1.1
X-Ver: realyagent.pl 0.01

同様に、 「ServerInput => sub {」から始まる部分が、 通信相手 (リレーサーバ) からデータを受信したときに実行するコードです。 受信したデータは、 いったん変数 $PollBuf に溜めておいて、 続いて呼び出す doPoll の中で処理を行ないます。

以上からお分かりのように、 リレーサーバへデータを送るときは、
「$PollHeap->{server}->put(送るべきデータ);」を実行し、 リレーサーバからデータが送られてきた時は、 doPoll で受取ります。 とても見通しが良いですね。

各 POEセッションは、スレッドと同様、同一メモリ空間を共有しているので、 他の POEセッションが変更した変数の値を参照できます。 したがってどの POEセッションでもリレーサーバへデータを送ることができますし、 リレーサーバから受信したデータはどの POEセッションでも読むことができます。

続いて、もう一つ POEセッションを作ります。

POE::Session->create
    ( inline_states =>
      { _start => sub {
          $_[KERNEL]->delay( tick => 10 );
        },
        tick => sub {
            if ($IdleTimer > 0) {
                if (--$IdleTimer <= 0) {
                    &sendControl(0, -2);        # keep alive
                }
            }
            $_[KERNEL]->delay( tick => 10 );
        },
      },
    );
$poe_kernel->run;
exit;

この POEセッションは 10秒に一回、 「tick => sub {」から始まる部分を実行します。 見ての通り、$IdleTimer の値を減らしていって、 0 になったら sendControl を実行します。 $IdleTimer は最初 6 ($IdleTimerMax) に設定されるので、 1 分ごとに sendControl を実行する、という意味ですね。

以上 2つの POEセッションは作成しただけで、まだ走り出していません。
その次の「$poe_kernel->run;」が各 POEセッションを走らせるための呼び出しです。 このルーチンは全ての POEセッションが終了するまで返ってきません。

さて、relayagent はリレーサーバとの接続を常時維持していますが、 無通信時間が続くと (通信経路中にあるファイアウォールなどに) 切られてしまう恐れがあるので、 keep alive ブロックを送信しています。 通信が行なわれていない時間を測るためのカウンタが $IdleTimer というわけです。

通信が行なわれない限り $IdleTimer は減り続け、 1 分経過すると sendControl(0, -2) を呼び出して keep alive ブロックを送信します。 sendControl はこんな感じ:

sub sendControl {
    my ($id, $control) = @_;
    $control += 65536 if $control < 0;
    $IdleTimer = $IdleTimerMax;
    if (defined $PollHeap && $PollHeap->{connected}) {
        $PollHeap->{server}->put(pack("nn", $id, $control));
    }
}

既に説明したように「$PollHeap->{server}->put(データ)」は、 リレーサーバにデータを送る呼び出しですから、 「pack("nn", 0, 65534)」が keep alive ブロックであることが分かります。

「ブロック」というのは VPN-Warp 用語でして、 relayagent とリレーサーバとの通信は、 基本的にこの「ブロック」を単位にして行ないます。 ブロックは次のような可変長のデータです。

    ┌───┬───┬───┬───┬───┬─≪─┬───┐
    │セッションID│ データ長  │  可変長データ   │
    └───┴───┴───┴───┴───┴─≫─┴───┘
          2バイト         2バイト      「データ長」バイト

「セッションID」および「データ長」は、ビッグエンディアンです。 つまり上位バイトが先に来ます。 データ長が 0 ないし負数の場合は、 「可変長データ」の部分は 0 バイトになります。

データ長が 0 ないし負数であるブロックは、 コントロール用のブロックで、 以下の意味を持っています:

データ長意味内容
0EOFWebセッションの終了を要求
-1ErrorWebセッションの異常終了を要求
-2Keep Alive無通信状態が続いたときに送信
-3X OFFWebセッションのデータ送信の一時停止を要求
-4X ONWebセッションのデータ送信の再開を要求

ブラウザ送ったリクエストを Webサーバに届け、 Webサーバのレスポンスをブラウザに返す一連の通信のことを、 ここでは「Webセッション」と呼ぶことにします。 つまり、 VPN-Warp が提供する仮想的な通信路 (トンネル) 上のセッションです。

VPN-Warp セッション

ブラウザがリレーサーバと通信するときの TCP/IPセッションと、 relayagent と Webサーバが通信するときの TCP/IPセッションを対応づけるのが、 セッションID です。 「セッション」という言葉が何度も出てきてややこしいですが、 「セッションID」の「セッション」は、 「Webセッション」の意味です。

リレーサーバと relayagent との間は、 複数の Webセッションを一本の TCP/IPセッションに相乗りさせるので、 そのとき各 Webセッションがこんがらないようにするために ブロックにはセッションID がつけられている、というわけです。

では、次はいよいよ relayagent の中核ルーチンである doPoll です:

sub doPoll {
    do {
        if (! defined $PollHeader) {
            if ($PollBuf =~ /\r\n\r\n/) {
                $PollHeader = $`;
                $PollBuf = $';
            }
        }
        return unless defined $PollHeader;
        my ($id, $len, $data) = unpack("nna*", $PollBuf);
        return unless defined $id && defined $len && $len ne "";
        if ($len > 32767) {
            $len -= 65536;
            $PollBuf = $data;
            if ($len == -1) {
                &closeWeb($id);
            }
        } elsif ($len > 0) {
            return unless defined $data && length($data) >= $len;
            ($data, $PollBuf) = unpack "a${len}a*", $data;
            &reqWeb($id, $data);
        } else {        # len == 0
            $PollBuf = $data;
            &closeWeb($id);
        }
    } while ($PollBuf);
}

前述したように、relayagent はリレーサーバに接続したとき、 まず
「GET /KLAB/poll HTTP/1.1」から始まるリクエストヘッダを送ります。 するとリレーサーバは、 次のようなレスポンスを返します:

HTTP/1.1 200 OK
X-Customer: nusers=5&type=1&expire=1169696110&digest=3f6977eceb8c2c43e28e6026b08ba900

そしてこの後 (doPoll において「defined $PollHeader」が真のとき)、 リレーサーバと relayagent は、 前述したブロックを送受信することになります。

「my ($id, $len, $data) = unpack("nna*", $PollBuf);」の部分が、
リレーサーバから受信したブロックを、
「セッションID ($id)」 「データ長 ($len)」 「可変長データ ($data)」 に分解している処理ですね。 続いてブロックの処理が行なわれますが、 コントロールブロックに関する処理は割愛して、 可変長データが付いているブロックの処理を見ていきましょう。 ここで受信した可変長データは、 ブラウザが送信した http リクエストを 2048バイトごとに分割したものです。

つまりリレーサーバは、 ブラウザから https リクエストを受取るたびに「セッションID」を割り振ります。 そして、リクエストをブロックに分割して relayagent へ送信し、 逆に relayagent から受取ったブロックを 同じセッションID ごとに連結して、 http レスポンスとしてブラウザへ送信します。

したがって、 relayagent はリレーサーバから受取ったブロックを 同じセッションID ごとに連結して Webサーバへ中継し、 そのレスポンスをブロックに分割してリレーサーバへ送信すればよいことになります。

同じセッションID ごとに連結して Webサーバへ送信する処理が、 reqWeb です:

sub reqWeb {
    my ($id, $req) = @_;
    if (defined $WebHeap{$id} && $WebHeap{$id}->{connected}) {
        $WebHeap{$id}->{server}->put($req);
    } else {
        POE::Component::Client::TCP->new
            ( RemoteAddress => $WebHost,
              RemotePort    => $WebPort,
              Connected     => sub {
                  $WebHeap{$id} = $_[HEAP];
                  $WebHeap{$id}->{server}->put($req);
              },
              ServerInput   => sub {
                  $WebHeap{$id} = $_[HEAP];
                  &sendRes($id, $_[ARG0]);
              },
              Filter        => POE::Filter::Stream->new(),
              Disconnected  => sub {
                  &sendControl($id, 0);
              },
            );
    }
}

「POE::Component::Client::TCP->new」によって、 Webサーバと通信するための POEセッションを生成しています。 この reqWeb を実行しているのは、 リレーサーバとの通信を受け持つ POEセッションでしたが、 この POEセッションが新たに POEセッションを生成している点に注意してください。

新しく生成した POEセッションは、Webサーバと接続したとき (Connected)、
「$WebHeap{$id}->{server}->put($req);」を実行して リクエスト ($req) を Webサーバに送信します。 そして Webサーバからレスポンスを受信したとき (ServerInput)、 sendRes を実行します。

sub sendRes {
    my ($id, $res) = @_;
    $IdleTimer = $IdleTimerMax;
    if (defined $PollHeap && $PollHeap->{connected}) {
        for my $block (unpack "(a2048)*", $res) {
            $PollHeap->{server}->
                put(pack("nna*", $id, length($block), $block));
        }
    }
}

sendRes は Webサーバからのレスポンス ($res) を 2048バイトごとに分割し、 セッションID ($id) とデータ長 (length($block)) を付加した ブロックとしてリレーサーバに送信します。

以上をまとめたのが、relayagent スクリプト です。 ここで解説した機能の他、 http リクエストヘッダの Host: フィールドを書き換える機能も追加しています。

C 版の relayagent に比べると、 http レスポンスの書き換え機能や、 http 以外のプロトコルを通す機能などがない点や、 高負荷時の性能の検証が充分行なえていない点など、 そのまま実運用に使用するには難しい点もありそうですが、 少なくとも プロトタイピングなどの目的 (あるいは教育などの目的) ならば 充分使えそうです。

Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 07:13
2006年10月6日

SED 教室

久しぶりに sed の話題を見かけたので、 思わずトラックバックしてみます。

アキバ系!文京区本郷四畳半社長」曰く

最近はSEDとかみんな使わないのかなあ 便利なのに
...
Rubyスクリプトさえ書きたくないときにはSedです。
Sedの過激な使い方については 往年の名著「MS-DOSを256倍使うための本 vol.2」が めっぽう面白い
これこそハックだよなあ

手前ミソながら、sed の過激な使い方にかけては、 SED 教室 も そこそこいい線行っているんじゃないかと自負しておりますが、 いかがでしょうか?

例えば、 SED 教室 第十一回 「正規表現、再論」 で紹介している、 sed で「uniq -c」コマンドを実現するスクリプト:

x
1s/.*/    /
H
y/ 0123456789/11234567890/
G
s/.*\([^0]0*\)\n.*\n\(.*\)[^9]9*$/\2\1/
x
s/\n.*//
$!N
/^\(.*\)\n\1$/!{
  x
  G
  s/\n/  /
  P
  s/.*/    /
  x
}
D

なんてのは、ぱっと見では何をやってるんだか、 まるで分からないこと請け合い ;-)

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 11:38
2006年9月27日

ssh-agent を screen の中から使う方法 hatena_b

GNU screenバグ報告を行なう ついでに screen-devel ML に参加したら、 次のようなメールが ML に流れてきた:

There is a much simpler solution
http://www.2701.org/archive/200406150000.html

The key is that SSH_AGENT need not point to a socket, it can point to a symbolic link to a socket.

なるほど~

ssh-agent と通信するための UNIX ドメイン ソケット を指す (パス名固定の) シンボリック リンクを作るようにしておけば、 環境変数 SSH_AUTH_SOCK には、そのシンボリック リンクのパス名を 設定しておけば済むので screen の中で ssh を使うとき便利、 というわけである。 つまり、

senri:/home/sengoku % ssh asao
Last login: Sun Sep 10 08:24:20 2006 from senri.flets.gcd.org
Linux 2.6.16.28.

asao:/home/sengoku % echo $SSH_AUTH_SOCK
/tmp/ssh-chKJY25976/agent.25976
asao:/home/sengoku % screen -r

senri で ssh-agent を走らせておいて、 asao へ ssh でログインするさいに、 ForwardAgent を有効にしておくと、 上の実行例のように、SSH_AUTH_SOCK に UNIX ドメイン ソケットの パス名が設定され、このソケットを介して senri の ssh-agent と通信ができる。

ところが、前回のログイン時に使っていた screen を reattach すると、 screen の中では、SSH_AUTH_SOCK の値は、 前回のログイン時のパス名のままである:

asao:/home/sengoku % echo $SSH_AUTH_SOCK
/tmp/ssh-ptnuvb3346/agent.3346

ForwardAgent はログアウトと共に終了するので、 screen の中の SSH_AUTH_SOCK の値は、 ログインするごとに設定し直す必要がある。 これはとてもメンドクサイ。

ログインし直すたびに SSH_AUTH_SOCK の値が変化するから、 このような問題が起きるわけで、 SSH_AUTH_SOCK の値が常に同じなら、 reattach した screen の中でも同じ SSH_AUTH_SOCK の値を使い続けることができる。

すなわち、 SSH_AUTH_SOCK が直接 UNIX ドメイン ソケットを指し示すのではなく、 UNIX ドメイン ソケットを指し示すシンボリック リンクを作成しておいて、 SSH_AUTH_SOCK にはこのシンボリック リンクのパス名を設定しておけばよい。

さっそく ~/.cshrc に次の行を追加した:

set agent = "$HOME/tmp/ssh-agent-$USER"
if ($?SSH_AUTH_SOCK) then
        if (! -S $SSH_AUTH_SOCK) unsetenv SSH_AUTH_SOCK
endif
if ($?SSH_AUTH_SOCK) then
        if ($SSH_AUTH_SOCK =~ /tmp/*/agent.[0-9]*) then
                ln -snf "$SSH_AUTH_SOCK" $agent && setenv SSH_AUTH_SOCK $agent
        endif
else if (-S $agent) then
        setenv SSH_AUTH_SOCK $agent
else
        echo "no ssh-agent"
endif
unset agent

私は、かれこれ 20年近く csh をログイン シェルとして使い続けてきているので、 ~/.cshrc なのだが、今となっては (極めて?) 少数派だろう。 bash など、sh 系をログイン シェルとして使っている場合は、 ~/.profile などに

agent="$HOME/tmp/ssh-agent-$USER"
if [ -S "$SSH_AUTH_SOCK" ]; then
        case $SSH_AUTH_SOCK in
        /tmp/*/agent.[0-9]*)
                ln -snf "$SSH_AUTH_SOCK" $agent && export SSH_AUTH_SOCK=$agent
        esac
elif [ -S $agent ]; then
        export SSH_AUTH_SOCK=$agent
else
        echo "no ssh-agent"
fi

などと書いておけばよいだろう。

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 07:56
2006年8月28日

screen 4.0.2 のバグ hatena_b

GNU screen の 最新バージョンである 4.0.2 において、 SJIS な端末で screen を走らせて screen のウィンドウで eucJP を使おうとすると、 1 バイト文字の前に 0x8E が挿入されてしまう。
つまり、 screen のコマンド「encoding eucJP SJIS」 (kanji euc sjis) を実行した場合とか、
あるいは「KJ=SJIS」を指定した端末で「defencoding eucJP」 (defkanji euc) を指定した screen を使うといった場合である。

なぜだろうと思い、ソースを確認すると、encoding.c の 1154 行目あたりが 次のようになっている:

          if ((0x81 <= c && c <= 0x9f) || (0xe0 <= c && c <= 0xef))
            {
              *statep = c;
              return -1;
            }
          return c | (KANA << 16);

え? これってもしかして 2 バイト文字 (全角文字) 以外は 全て 1 バイトカナ (半角カナ) 扱いにしてしまっている?

screen 4.0.2 は、2004年1月27日に公開されていて (私の知る限り) これが最新版だと思うのだが、 このような単純なバグが 2年以上にわたって放置されているとは信じられないので、 すでにパッチが出回っていて、 開発元にも連絡が行っているのではないかと思う。 ご存じの方はご指摘頂ければ幸いである。 しばらく様子をみて、ご指摘が無いようであれば、 念のため開発元にパッチを送ってみる予定。

言うまでもなく、0x80 未満 (最上位ビットが 0) の文字は、 「KANA」扱いしてはいけないので、 上記コードは以下のようであるべきだ:

          if ((0x81 <= c && c <= 0x9f) || (0xe0 <= c && c <= 0xef))
            {
              *statep = c;
              return -1;
            }
          if (!(c & 0x80)) return c;
          return c | (KANA << 16);

このような修正を加えることにより、例えば ~/.screenrc

defkanji euc
terminfo xterm KJ=sjis
terminfo kterm KJ=euc
terminfo vt100 AB=\E[4%p1%dm:AF=\E[3%p1%dm:KJ=euc

などと設定して、term=xterm な端末 (term は xterm であるが「シフトJIS」な漢字を表示できる) を使うような場合でも、 漢字を正しく表示できるようになった。

Linux の多くは EUC を標準的な漢字コードとして使っているはずで、 その一方で Windows は SJIS が標準的な漢字コードだったはず (最近は UTF8 の方が多い?) なので、 このような SJIS な端末で EUC な screen を使うケースは 決してレアケースではないと思うのだが、 なぜこのようなバグが放置されていたのかとても不思議である (ちなみに私は Windows 上の TeraTerm を EUC の設定で使っていたため、 このバグに今まで気づかなかった)。

このバグは、端末の漢字コードが SJIS で、かつ screen のウィンドウの漢字コードが SJIS 以外の場合 (つまりコード変換が行なわれる場合) に発現する。 全ての 1 バイトコード (0x00 ~ 0x1F のコントロールコードさえも!) の 前に「0x8E」をつけてくれるので、 screen の detach すらできなくなるという凶悪なものである。
念のため screen 4.0.2 に対する patch の形で修正点を示しておく:

--- encoding.c.org        Mon Sep  8 23:25:23 2003
+++ encoding.c        Mon Aug 28 18:11:57 2006
@@ -1151,6 +1151,7 @@
               *statep = c;
               return -1;
             }
+          if (!(c & 0x80)) return c;
           return c | (KANA << 16);
         }
       t = c;
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 19:55
2006年5月10日

時刻表ビューアを Wrist PDA に移植して頂いた

拙作 時刻表ビューア を、 Fossil/ABACUSのPalmOSが搭載された腕時計 WristPDA(腕パーム)移植して頂いた

私は、時刻表ビューアのバージョンアップは 2000年8月を最後に行っていない。 その後、Linux Zaurus SL-C700 を使うようになってからは、 Palm 自体を使わなくなってしまっていたので、 時刻表ビューアを見ることさえなくなってしまっていた。

作者自身が忘れていたソフトウェアを、 新しい機種に移植して利用していただけたというのは 大変嬉しいことであり、 オープンソース化しておいて 本当によかったと思う。

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 08:02
2006年5月8日

Emacsでtrampを使って /su:root@localhost:/ をアクセスする hatena_b

先日書いた「su & emacsclient」にトラックバックを頂きました (_O_)。曰く:

tramp は /su: や /sudo: なパスを扱う場合は内部で su や sudo を使うだけで、 なんでもかんでも ssh を使うわけではないです。

思い切り誤解してました。orz
私の場合、su は opie 使っていて、

senri:/home/sengoku % su
otp-md5 416 se2369 ext
root's response:

などとなるので、

(setq tramp-password-prompt-regexp
"^.*\\([pP]assword\\|passphrase.*\\|\n.*response\\):^@? *")

と設定することにより、 チャレンジの部分を含めてプロンプトに出すことができて、 無事 tramp & su で root 権限でファイルを編集することができました。 長年(?)の懸案が解決しました(_O_)。

日記で質問してよかった~

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 17:49
2006年5月5日

su & emacsclient hatena_b

普段 emacs を使っている人に質問なのですが、
root 作業するときどうしてますか?

私は、GNU Screen の中で emacs をずーっと立ち上げっぱなしにしていて、 ほとんどの作業を emacs の中で行なっています。 もちろんコマンドラインから何かを実行するときも、 emacs の shell モード (正確に言うと j-shell.el なんですが ^^;) の中で 行なっています。

いきおい、root になるときも shell モードで「su」を実行することになります。 で、root 権限でファイルを読み書きしようとしたとき、 どうするのがいいか、というのが問題です。

そんなの root で emacs を実行しておけばええやん、 という声が聞こえてきそうですが、 root 権限で常に emacs が動いている、というのは 想像するのもおぞましいですし、かといって 編集するたびに root で emacs を立ち上げるのは、 (起動に時間がかかるので) もっと嫌です。 そもそも root 権限で emacs (に限らずエディタならなんでも) を 立ち上げるには、 Screen の別ウィンドウで行なわなければならず、 ウィンドウの切替で作業がかなり煩雑になってしまいます。

ちなみに、ずーっと以前は、ange-ftp を使っていました。 つまり、emacs から root@localhost へ ftp して (root の) ファイルを読んできて編集し、 保存するときも ftp で書込む。 この方法は root でログインする、 という気持ち悪さがもともとあったので、 ftpd を走らせなくなったのを機会に止めました。

で、それ以来使っているのが、 今回紹介する emacsclient を使う方法です。 もしもっといい方法があるぞっ、というかたがいらっしゃいましたら、 是非教えてください (_O_)。

- o -

emacs 上で「M-x server-start」と入力すると、 emacsserver (gnuserv) を 走らせておくことができます。 この状態で、コマンドラインから

% emacsclient ファイル名

などと emacsclient (gnuclient) を実行すると、 引数に指定したファイルを、emacs で編集することができます。

したがって、root 権限でファイルを編集するときは、 まずファイルを emacs で読み書きできるようテンポラリファイルへコピーし、 それを emacsclient で開き、 テンポラリファイルが変更されたら、 それを元のファイルへ root 権限で書き出せばよいことになります。

私は suemacs と名付けた以下のような perl スクリプトを書いて使っています。 例えば

# suemacs /root/.cshrc

などと実行すると、 /root/.cshrc の内容が /tmp/suemacs5544/.cshrc_0 へコピーされ、 emacsclient /tmp/suemacs5544/.cshrc_0が実行されます。

suemacs スクリプト

#!/usr/bin/perl
$user = $ENV{'LOGNAME'};
$tmp = "/tmp/suemacs$$";
$Debug = 0;
$Once = 0;
use POSIX ":sys_wait_h";
use Getopt::Std;
getopts('do') || &help;
$Debug = 1 if $opt_d;
$Once = 1 if $opt_o;

sub help {
    print <<EOF;
Usage: suemacs <opt> <file>...
opt:  -o        ; write once on closing
      -d        ; for debug
EOF
    exit 1;
}

if ($> != 0 || ! $user) {
    exec "emacsclient", @ARGV;
}
($login, $pass, $uid, $gid) = getpwnam($user) or die;
print "login: $login,  uid: $uid,  gid: $gid\n" if $Debug;
umask 077;
mkdir $tmp || die;
chown $uid, $gid, $tmp;

for ($i=0; $i < @ARGV; $i++) {
    my $tmpfile = $ARGV[$i];
    $tmpfile =~ s@.*/@@;
    $tmpfile = "$tmp/${tmpfile}_$i";
    push @argv, $tmpfile;
    my $mtime = &cp($ARGV[$i], 0, $tmpfile);
    push @mtime, $mtime;
    chown $uid, $gid, $tmpfile;
}
if (!$Debug) {
    if (!fork) {
        close(STDOUT);
        close(STDERR);
        open(">&STDOUT", "/dev/null") || die;
        open(">&STDERR", "/dev/null") || die;
    } else {
        exit 0;
    }
}
if (!fork) {
    ($(, $)) = ($gid, $gid);
    ($<, $>) = ($uid, $uid);
    exec "emacsclient", @argv;
    exit 0;
}
my ($ret);
do {
    sleep 1;
    $ret = waitpid(-1,WNOHANG);
    print "ret: $ret, status: $?\n" if $ret > 0 && $Debug;
    for ($i=0; $i < @argv; $i++) {
        $mtime[$i] = &cp($argv[$i], $mtime[$i], $ARGV[$i])
            if $ret > 0 || ! $Once;
        unlink $argv[$i] if $ret > 0;
    }
} until ($ret > 0);
rmdir $tmp;
exit 0;

sub cp {
    my ($src, $stime, $dst) = @_;
    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
        $atime,$mtime,$ctime,$blksize,$blocks) = stat($src);
    if ($stime == $mtime) {
        print "$src is not modified.\n" if $Debug;
        return $mtime;
    }
    print "cp $src $dst\n" if $Debug;
    open(SRC, $src) || die;
    open(DST, ">$dst") || die;
    while (<SRC>) {
        print DST;
    }
    close(DST);
    close(SRC);
    utime $atime, $mtime, $dst;
    return $mtime;
}
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 19:35
2006年5月1日

livedoor blog 生ログ取得スクリプト (2)

昨日書いた「livedoor blog 生ログ取得スクリプト」を使って、 毎日前日の生ログを取得する cron を仕掛けておいたのですが、 月が代わった途端に問題発覚(^^;)。 livedoor blog の生ログって、月単位なのですね。 つまり、生ログを参照するページの URL は、

http://analyzer.livedoor.com/log/raw?page_id=22222&y=2006&m=4

などとなっていますが、「m=4」を指定しない限り 4月分のログは表示されない (今月分のみ表示される)、 という仕様のようです。 livedoor blog の有料プランを始めてから初めての月代わりだったもので...

というわけで、生ログを取得する際に日付を指定できるように修正してみました。

livedoor.pl gcd date 2006-04-30 raw_log 100 -

などと、「date YYYY-MM-DD」ないし「date YYYY-MM」を指定することにより、 生ログを取得する日付ないし月を指定できます。 「raw_log 100 - 」は、生ログ 100ページ分 (2000行) を標準出力 (「 - 」) に出力する、という意味です。

生ログを遡っていく途中で、指定した日付と異なる日付の生ログが得られたら、 それ以上の取得をストップするので、ページ数は多めに指定しておいて大丈夫です。 また、ついでに「ブログのエクスポート(バックアップ)」もサポートしました。

livedoor.pl gcd export_reserve

で、「エクスポートファイルの作成」を行ない、

livedoor.pl gcd export バックアップファイル名

で、エクスポートファイルをダウンロードして 「バックアップファイル名」で保存します。

以上の機能は、 livedoor blog の有料プランを選択していないと使用できませんが、 スタイルシート(CSS)および HTMLのテンプレートの取得は、 ブログのデザインを custom に設定していれば使用できます。

まず、livedoor blog の管理ページで、 「カスタマイズ/管理」-「デザインの設定」を選び、 デザインとして「カスタマイズ」を選択したときの URL を確認します。 例えば

http://cms.blog.livedoor.com/cms/design/edit?blog_id=1600549&id=11111

といった感じの URL になると思います。 「blog_id=1600549」および「id=11111」の数字を、 スクリプト先頭の

"BlogID" => 1600549,
"ID"     => 11111,

の部分に設定しておいて、

livedoor.pl gcd css CSSファイル名

と実行すれば、スタイルシート(CSS) が「CSSファイル名」で保存されます。 「css」の代わりに、 「index_tmpl」「article_tmpl」「category_tmpl」「monthly_tmpl」 を指定すれば、それぞれ「トップページ」「個別記事ページ」 「カテゴリアーカイブ」「月別アーカイブ」の HTMLテンプレートを 保存できます。 一度のスクリプト実行で一括して保存することもできます:

livedoor.pl gcd css blog.css index_tmpl blog.html \
        article_tmpl blog_article.html \
        category_tmpl blog_category.html \
        monthly_tmpl blog_month.html

「スタイルシート(CSS)」を、「blog.css」へ、
「トップページ」を、「blog.html」へ、
「個別記事ページ」を、「blog_article.html」へ、
「カテゴリアーカイブ」を、「blog_category.html」へ、
「月別アーカイブ」を、「blog_month.html」へ、
それぞれ保存します。

livedoor.pl (livedoor blog 生ログ & CSS/テンプレート 取得スクリプト)

#!/usr/bin/perl
use LWP::UserAgent;
use HTTP::Request::Common;
use CGI qw/unescapeHTML/;

%blogs = (
    "gcd" => {
        "User"   => "hiroaki_sengoku",
        "Pass"   => "xxxxxxxx",
        "BlogID" => 1600549,
        "ID"     => 11111,
        "PageID" => 22222,
    },
    "klab" => {
        "User"   => "klab_sengoku",
        "Pass"   => "yyyyyyyy",
        "BlogID" => 1631449,
        "ID"     => 33333,
        "PageID" => 44444,
    },
);

&help unless $_ = shift;
if (exists $blogs{$_}) {
    my $blog = $blogs{$_};
    $User =   $$blog{"User"};
    $Pass =   $$blog{"Pass"};
    $BlogID = $$blog{"BlogID"};
    $ID =     $$blog{"ID"};
    $PageID = $$blog{"PageID"};
} else {
    &help;
}

$ua = new LWP::UserAgent;
$ua->agent("Mozilla/5.0 (Windows; U; Windows NT 5.1; ja)");
$ua->env_proxy();
$ua->cookie_jar( {} );
my $res = $ua->request(POST "http://member.livedoor.com/login/index",
                       [ "livedoor_id" => $User, "PASSWORD" => $Pass,
                         ".next" => "", ".sv" => "" ]);
while (my $type = shift) {
    if ($type eq "date") {
        $_ = shift;
        if (/^(\d\d\d\d)-(\d\d)(?:-(\d\d))?$/) {
            $Year = $1;
            $Month = ($2 + 0);
            $Date = $_;
        } else {
            die "date must be YYYY-MM-DD: $_\n";
        }
    } elsif ($type eq "css" ||
             $type eq "index_tmpl" || $type eq "article_tmpl" ||
             $type eq "category_tmpl" || $type eq "monthly_tmpl") {
        my $file = shift;
        open(OUT, ">$file") || die;
        my $url = "http://cms.blog.livedoor.com/cms/design/edit"
            . "?tmpl=$type&blog_id=$BlogID&id=$ID";
        my $req = new HTTP::Request GET => $url;
        my $res = $ua->request($req);
        if ($res->content =~
            /\<textarea .*name=\"content\" [^\>]*\>([^\<]+)\<\/textarea\>/) {
            my $content = unescapeHTML($1);
            $content =~ s/\r\n/\n/g;
            print OUT $content, "\n";
        }
        close(OUT);
    } elsif ($type eq "raw_log") {
        my $npage = shift;
        ($npage =~ m/^\d+$/ && $npage >= 1) || &help;
        my $file = shift;
        &help unless $file;
        open(OUT, ">$file") || die;
        my $url = "http://analyzer.livedoor.com/log/raw?page_id=$PageID";
        if ($Date) {
            $url .= "&y=$Year&m=$Month";
        }
        my $prepat = '\<td\b[^\>]*\>\<strong\>\<small\>';
        my $postpat = '\<\/small\>\<\/strong\>\<\/td\b[^\>]*\>';
        my $datematch = 0;
        pages: for (my $i=1; $i <= $npage; $i++) {
            my $req = new HTTP::Request GET => "$url&p=$i";
            my $res = $ua->request($req);
            my $datepat = '\d\d\d\d\-\d\d\-\d\d \d\d\:\d\d\:\d\d';
            my $date;
            for (split(/(\<small\>$datepat\<\/small\>)/o, $res->content)) {
                if (/^\<small\>($datepat)\<\/small\>$/o) {
                    $date = $1;
                } elsif (/^\<\/th\>\s*\<\/tr\>\s*/) {
                    my @record;
                    for (split(/\<\/tr\>\s*/, $')) {
                        my $column;
                        if (/$prepat(.*)$postpat/o) {
                            if ($1 eq 'URL') {
                                $column = 0;
                            } elsif ($1 eq 'リファラ') {
                                $column = 1;
                            } elsif ($1 eq 'ブラウザ') {
                                $column = 2;
                            } elsif ($1 eq 'リモートホスト') {
                                $column = 3;
                            } else {
                                die "Unknown column: $_\n";
                            }
                        }
                        if (/\<td\b[^\>]*\>\<small\>(.*)\<\/small\>\<\/td\b[^\>]*\>/){
                            $_ = $1;
                            s/\<\/?a\b[^\>]*\>//g;
                            if (/,/) {
                                s/\"/\"\"/g;
                                $_ = "\"$_\"";
                            }
                            $record[$column] = $_;
                        } elsif (/^\<\/table\>/) {
                            last;
                        } elsif (! /^\<tr\>\s*\<th\b[^\>]*\>/) {
                            die "Unknown format: $_\n";
                        }
                    }
                    if (! defined($Date) || $date =~ /^$Date/) {
                        $datematch = 1;
                        print OUT $date, ",", join(',', @record), "\r\n";
                    } elsif ($datematch) {
                        last pages;
                    }
                }
            }
        }
        close(OUT);
    } elsif ($type eq "export_reserve") {
        my $url = "http://cms.blog.livedoor.com/cms/import/mt/export_reserve";
        my $req = new HTTP::Request GET => $url;
        my $res = $ua->request($req);
        if (! $res->is_success) {
            print STDERR "fail to reserve export";
            exit 1;
        }
    } elsif ($type eq "export") {
        my $file = shift;
        open(OUT, ">$file") || die;
        my $url = "http://cms.blog.livedoor.com/cms/import/mt/export";
        my $req = new HTTP::Request GET => $url;
        my $res = $ua->request($req);
        print OUT $res->content;
        close(OUT);
    } else {
        &help;
    }
}
exit 0;

sub help {
    print STDERR "Usage livedoor <blog> <opt>...\nblog: ",
    join("\n      ", keys %blogs), "\n",
    'opt:  date YYYY-MM
      date YYYY-MM-DD
      css <file>
      index_tmpl <file>
      article_tmpl <file>
      category_tmpl <file>
      monthly_tmpl <file>
      raw_log <n> <file>
      export_reserve
      export <file>
';
    exit 1;
}
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 08:45
2006年4月30日

livedoor blog 生ログ取得スクリプト (1)

すでに誰かが絶対に書いているはずとは思ったのですが、 探すよりも書いた方が早そうだったので、 livedoor ブログの生ログを取得する perl スクリプトを 書いてみました。 ついでに、デザインをカスタマイズしたときの、 スタイルシート(CSS)やHTMLのテンプレートも取得できます。 例えば、

livedoor.pl gcd raw_log 10 log.csv

などと実行すれば、10 ページ分 (200行) の生ログを、 CSV 形式でファイル「log.csv」に保存できます。 また、

livedoor.pl gcd index_tmpl index.html

などと実行すれば、インデックスページのHTMLテンプレートを、 ファイル「index.html」に保存できます。 第一引数「gcd」の部分には、 ブログのアカウント名 (スクリプトの先頭部分で定義しています) を 指定してください。 私の場合、 「GCD 日記」と 「仙石浩明CTO の日記」の 二つのブログアカウントがあるので、 それぞれ「gcd」と「klab」という名前で定義しています。

余談ですが、二つのブログを書いているのは、 個人用と会社用とを区別しようというわけではありません。 もともと私のなかでは趣味と仕事の境界線が曖昧なので、 個人と会社でブログを区別しようとしても混ざってしまうでしょうから、 区別することに意味があるとは思えません。 じゃ、なぜ二つのブログなのかと言えば、 「GCD 日記」のほうが よりメモ的でネタを蓄えておき、ある程度考えがまとまったものを 「仙石浩明CTO の日記」へ 書こう、というのが そもそもの意図でした。

やっつけ仕事なので、突っ込みどころ満載(^^;) のスクリプトだとは思いますが、 livedoorブログをお使いの方はご利用頂ければ幸いです。 もちろん、ご利用の際は先頭部分のユーザID & パスワード等を 適宜修正してください。 また、スクリプト中で日本語を使っているので、 このスクリプトは EUC-JP で保存する必要があります。

livedoor.pl (livedoor blog 生ログ & CSS/テンプレート 取得スクリプト)

#!/usr/bin/perl
use LWP::UserAgent;
use HTTP::Request::Common;
use CGI qw/unescapeHTML/;

%blogs = (
    "gcd" => {
        "User"   => "hiroaki_sengoku",
        "Pass"   => "xxxxxxxx",
        "BlogID" => 1600549,
        "ID"     => 11111,
        "PageID" => 22222,
    },
    "klab" => {
        "User"   => "klab_sengoku",
        "Pass"   => "yyyyyyyy",
        "BlogID" => 1631449,
        "ID"     => 33333,
        "PageID" => 44444,
    },
);

&help unless $_ = shift;
if (my $blog = $blogs{$_}) {
    $User =   $$blog{"User"};
    $Pass =   $$blog{"Pass"};
    $BlogID = $$blog{"BlogID"};
    $ID =     $$blog{"ID"};
    $PageID = $$blog{"PageID"};
} else {
    &help;
}

$ua = new LWP::UserAgent;
$ua->agent("Mozilla/5.0 (Windows; U; Windows NT 5.1; ja)");
$ua->env_proxy();
$ua->cookie_jar( {} );
my $res = $ua->request(POST "http://member.livedoor.com/login/index",
                       [ "livedoor_id" => $User, "PASSWORD" => $Pass,
                         ".next" => "", ".sv" => "" ]);
while (my $type = shift) {
    if ($type eq "css" || $type eq "index_tmpl" || $type eq "article_tmpl" ||
        $type eq "category_tmpl" || $type eq "monthly_tmpl") {
        my $file = shift;
        open(OUT, ">$file") || die;
        my $url = "http://cms.blog.livedoor.com/cms/design/edit"
            . "?tmpl=$type&blog_id=$BlogID&id=$ID";
        my $req = new HTTP::Request GET => $url;
        my $res = $ua->request($req);
        if ($res->content =~
            /\<textarea .*name=\"content\" [^\>]*\>([^\<]+)\<\/textarea\>/) {
            my $content = unescapeHTML($1);
            $content =~ s/\r\n/\n/g;
            print OUT $content, "\n";
        }
        close(OUT);
    } elsif ($type eq "raw_log") {
        my $npage = shift;
        ($npage =~ m/^\d+$/ && $npage >= 1) || &help;
        my $file = shift;
        open(OUT, ">$file") || die;
        my $url = "http://analyzer.livedoor.com/log/raw?page_id=$PageID";
        my $prepat = '\<td\b[^\>]*\>\<strong\>\<small\>';
        my $postpat = '\<\/small\>\<\/strong\>\<\/td\b[^\>]*\>';
        for (my $i=1; $i <= $npage; $i++) {
            my $req = new HTTP::Request GET => "$url&p=$i";
            my $res = $ua->request($req);
            my $datepat = '\d\d\d\d\-\d\d\-\d\d \d\d\:\d\d\:\d\d';
            my $date;
            for (split(/(\<small\>$datepat\<\/small\>)/, $res->content)) {
                if (/^\<small\>($datepat)\<\/small\>$/) {
                    $date = $1;
                } elsif (/^\<\/th\>\s*\<\/tr\>\s*/) {
                    my @record;
                    for (split(/\<\/tr\>\s*/, $')) {
                        my $column;
                        if (/$prepat(.*)$postpat/o) {
                            if ($1 eq 'URL') {
                                $column = 0;
                            } elsif ($1 eq 'リファラ') {
                                $column = 1;
                            } elsif ($1 eq 'ブラウザ') {
                                $column = 2;
                            } elsif ($1 eq 'リモートホスト') {
                                $column = 3;
                            } else {
                                die "Unknown column: $_\n";
                            }
                        }
                        if (/\<td\b[^\>]*\>\<small\>(.*)\<\/small\>\<\/td\b[^\>]*\>/){
                            $_ = $1;
                            s/\<\/?a\b[^\>]*\>//g;
                            if (/,/) {
                                s/\"/\"\"/g;
                                $_ = "\"$_\"";
                            }
                            $record[$column] = $_;
                        } elsif (/^\<\/table\>/) {
                            last;
                        } elsif (! /^\<tr\>\s*\<th\b[^\>]*\>/) {
                            die "Unknown format: $_\n";
                        }
                    }
                    print OUT $date, ",", join(',', @record), "\r\n";
                }
            }
        }
        close(OUT);
    } else {
        &help;
    }
}
exit 0;

sub help {
    print "Usage livedoor <blog> <opt>...\nblog: ",
    join("\n      ", keys %blogs), "\n",
    'opt:  css <file>
      index_tmpl <file>
      article_tmpl <file>
      category_tmpl <file>
      monthly_tmpl <file>
      raw_log <n> <file>
';
    exit 1;
}
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 09:07
2006年4月28日

Haskell

遅ればせながら Haskell で遊んでいます。 KLab の技術者の中にも、手続き型言語の世界に どっぷりつかっていて他の世界を知らない人は いるので、 tech ML (技術者向の KLab 社内メーリングリスト) で Haskell の紹介をしてみました。

~~ tech ML に投げたメールここから ~~
Subject: [tech:8480] Haskell

仙石です。

唐突ですが、Haskell って知ってますか?

私は面接した人に教えてもらった ;) のですが、最近流行りの関数型言語です。
ブログを見てると、あちこちで話題になっていますね。
入門用のページ:

  やさしい Haskell 入門 (バージョン98)
  http://www.sampou.org/haskell/tutorial-j/index.html

「やさしい」と書いてますが、関数型言語を初めて学ぼうとする人には敷居が
高いかも知れません。まずは簡単な Haskell プログラムを見てみましょう。

------------------------------------------------------------------------
guusuu x
  | x `mod` 2 == 0  =  True
  | otherwise       =  False
------------------------------------------------------------------------

これは guusuu(x) という関数の定義です。

  x を 2 で割った余りが 0 ならば、guusuu(x) = True
  それ以外ならば、                guusuu(x) = False

と読みます。簡単ですね? ;)
早速実行してみましょう。
上記プログラムを test.hs というファイル名で保存しておいて、
Haskell 処理系である ghci コマンドを実行します。

------------------------------------------------------------------------
senri:/home/sengoku/tmp % ghci
   ___         ___ _
  / _ \ /\  /\/ __(_)
 / /_\// /_/ / /  | |      GHC Interactive, version 6.4.2, for Haskell 98.
/ /_\\/ __  / /___| |      http://www.haskell.org/ghc/
\____/\/ /_/\____/|_|      Type :? for help.

Loading package base-1.0 ... linking ... done.
Prelude> :load test.hs
Compiling Main             ( test.hs, interpreted )
Ok, modules loaded: Main.
*Main> guusuu 2
True
*Main> guusuu 7
False
*Main>
------------------------------------------------------------------------

「:load test.hs」というのが「test.hs」を読み込むためのコマンドです。
「guusuu 2」を実行すると、guusuu(2) の値である True が出力されていますね。
これだけだと、あまり能がないので、偶数列を表示させてみましょうか。

------------------------------------------------------------------------
*Main> take 10 [1,2..]
[1,2,3,4,5,6,7,8,9,10]
*Main> take 10 [x|x <- [1,2..], guusuu x]
[2,4,6,8,10,12,14,16,18,20]
------------------------------------------------------------------------

「take 10」というのはその後ろのリストの先頭 10 個の要素を取り出す関数で
す。「[1,2..]」というのは自然数列のリストですね。take を使わずに [1,2..]
を表示させようとすると、無限に自然数列を表示します。

------------------------------------------------------------------------
*Main> [1,2..]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22, ...無限に続く...
------------------------------------------------------------------------

途中で止めるには control-C を押します。
さて、

  [x|x <- [1,2..], guusuu x]

という書き方は、グラフ理論の輪講に参加している人や、述語論理を学んだこと
のある人にはおなじみの書き方じゃないでしょうか。自然数列の中で、
述語 guusuu(x) が True であるような x のみ取り出したリスト、という意味です。

じゃ、このプログラムはどうでしょう?

------------------------------------------------------------------------
hurui [] = []
hurui (top:rest) = top:(hurui [x|x <- rest, x `mod` top /= 0])
------------------------------------------------------------------------

関数 hurui は引数としてリストをとります。「[]」は空リストです。つまり要
素が何もないリストですね。引数が空リストならば hurui [] の値も [] です。

引数が [] でない場合は、引数のリストを、先頭 top と残り rest に分解します。
例えば hurui [3,5,9,11,13] の場合、top が 3 で rest が [5,9,11,13] です。

# このあたり、lisp を知っている人にはおなじみの概念ですね

次に top と (hurui [x|x <- rest, x `mod` top /= 0]) をつなげたリストを、
hurui の値として返します。「:」がリストを作るための演算子です。

# lisp で言うところの cons と言えば lisp を知っている人には簡単ですね

では (hurui [x|x <- rest, x `mod` top /= 0]) とは何でしょう?

「/=」というのは等しくない、という演算子です。C で言うところの「!=」です
ね。つまり、rest の中で「x `mod` top /= 0」が True になるものを取り出し
たリストを求め、これを引数として hurui を再帰呼出しして求めたリスト、と
いうことになります。

top が 3 で rest が [5,9,11,13] でしたから、3 で割って余りが 0 でない
(つまり 3 で割り切れない) もののリスト、ということになります。9 以外は 3
で割り切れないので [5,11,13] ですね。これを引数として hurui に与えます。
つまり、3 (top の値) と hurui [5,11,13] の値をつなげたリストが答になりま
す。

同様に hurui [5,11,13] の値は、5 と hurui [11,13] (11 も 13 も 5 では割
り切れないから) の値をつなげたリストですね。というのをどんどん再帰的に繰
り返すと、hurui [3,5,9,11,13] の値は [3,5,11,13] になります。実際に試し
てみましょう:

------------------------------------------------------------------------
*Main> hurui [3,5,9,11,13]
[3,5,11,13]
------------------------------------------------------------------------

スルドイ人はすでに分かっていると思いますが、
この関数は「エラトステネスの篩」です。したがって、

------------------------------------------------------------------------
*Main> take 20 (2:hurui [3,5..])
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71]
------------------------------------------------------------------------

などと実行することにより、20個の素数を列挙することができました。

どうです? 面白いでしょう?

関数型言語を知っている人は、たぶん Haskell もすぐ使いこなすことができる
と思いますし、関数型言語を知らない人は、ぜひこの機会に知ることをオススメ
します。なぜなら関数型言語を知らないプログラマってプログラミング言語の世
界の半分しか知らないわけで、Haskell を学ぶことにより世界が大きく広がると
思うからです。

関数型言語を初めて学ぶ人は、まずは

  入門Haskell―はじめて学ぶ関数型言語
  向井 淳 (著)

あたりを読むのがいいかも知れません。私の机の上に置いておくので、興味ある
かたはどーぞ (先着一名様限定)。

この本は、副題にもあるように関数型言語を初めて学ぶ人向けに書かれているの
で、イマイチ本質を外しているんですよねぇ... だから本音ではオススメな本で
はない ;-) のですが、関数型言語へのとっかかりとしてはよいのかも知れません。

#13425                                                          仙石 浩明
https://www.gcd.org/sengoku/             Hiroaki Sengoku <sengoku@gcd.org>
~~ tech ML に投げたメールここまで ~~

このメールに一番早く反応したのは、KLab の開発に参加いただいている 協力会社 H 社の CTO の T さんでした。 T さんとはご無沙汰していたのですが、 彼も私と同様に Haskell で遊んでいたことが分かって、 さすがと思った次第。

# 協力会社さんに負けずに頑張ってね > KLab 社員

~~ T さんのメール(一部抜粋)ここから ~~
ご無沙汰しております。
H 社の T です。

Hiroaki Sengoku wrote:
> 唐突ですが、Haskell って知ってますか?

なぜか私も、今月の頭ぐらいにとあるブログで知って(YAPC::Asia 2006のPugs関連
のエントリだったような…)

>   入門Haskell―はじめて学ぶ関数型言語
>   向井 淳 (著)

を購入して、ひそやかに楽しんでおりました。
そして、いやあ、これは、自分だけ楽しんでいるには余りにももったいないので、
(H社内の)勉強会のネタにしようと宣言していた矢先に、投稿を拝見しまして、
つい反応してしまいました。
~~ T さんのメール(一部抜粋)ここまで ~~

KLab でも Haskell 勉強会やりましょう!

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 06:32
2006年4月22日

開発環境とプログラミング能力

開発環境の進化が、 プログラマのプログラミング能力を退化させていると思う。 私は、いわゆる統合開発環境というものを使ったことがなく、 いつでも emacs を愛用している。 しかも画面サイズは 20年来 80桁x24行のままである。

プログラミングは、メモを書きながら設計したあと、一気に書く。 設計さえきちんとできていれば、途中で手が止まることはあまりない。 食事も忘れて何時間も没頭することがよくある。 そして書き終えてたらコンパイル。 タイプミスとか変数宣言し忘れとかで出たコンパイルエラーを ひとつずつ修正。

で、コンパイルに成功したら実行させる。多くの場合、これで動く。 一通り動作確認して、期待しない動作をするところがあっても、 ほとんどの場合ソースを参照するまでもなく原因に思い当たる。 たいていの場合、デバッガを使うまでもなく、 ソースを見直すだけでどう修正すべきかも分かる。

という話をすると、奇異な目で見られてしまう。(^^;)
目視だけでデバッグと題するページで、 私と同じような感覚の人を見つけて安心した。

たいていの人は、デバッガでステップ実行させて、 実行中の変数を参照したり、 値を変えてみたりしてプログラムを修正するのだという。 たしかに頭の中でプログラムの動作を追うより、 デバッガを使って実際に動かしてみるほうが楽かもしれないが、 それでプログラミングスキルが伸びるのだろうか?

まるで、将棋を指すとき対戦用の盤面とは別に、 相手の指し手の可能性を検討する盤面を脇に置いて、 次の一手を検討しているようなものではないか。 そんなことをしていたら、 次の一手を考えるのに膨大な時間がかかってしまうし、 頭が鍛えられないので上達も難しいだろう。

プログラミングも同じ事。 頭の中に仮想的にデバッガを構築して、 無意識の思考でプログラムを実行させることができなければ、 いつになってもプログラムを見通す洞察力は身につかないだろう。

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 11:24
2006年3月30日

glibc 2.3 での IPv6

一年以上前に tech ML で取り上げたネタなのですが、現在よく使われている Linux ディストリビューションでも glibc 2.3.2 あたりが使われることもある ようなので紹介します。


KLab のイントラのサーバには、IPv6 なアドレスも割り当ててあります。 例えば、

% host kamiya.v6.klab.org
kamiya.v6.klab.org has IPv6 address 2001:c90:c1c:100e:2e0:81ff:feab:cdef

% host 2001:c90:c1c:100e:2e0:81ff:feab:cdef
f.e.d.c.b.a.e.f.f.f.1.8.0.e.2.0.e.0.0.1.c.1.c.0.0.9.c.0.1.0.0.2.ip6.arpa domain name pointer kamiya.v6.klab.org.

のような感じ。kamiya というホスト名は KLab が六本木ヒルズへ移転してくる 前は、神谷町にオフィスがあったことにちなんでいます。イントラのサーバ群に は地名がつけられているマシンが多く、もちろん roppongi というホスト名の マシンもあります。

で、当時は Linux サーバの多くは glibc 2.2 を使っていたのですが、一部の マシンは glibc 2.3 にバージョンアップしていました。ところが、glibc 2.3 な マシンから ping6 を打ってみると異様に遅い...

% time ping6 -c 3 kamiya.v6.klab.org
PING kamiya.v6.klab.org(kamiya.v6.klab.org) 56 data bytes
64 bytes from kamiya.v6.klab.org: icmp_seq=1 ttl=64 time=1.10 ms
64 bytes from kamiya.v6.klab.org: icmp_seq=2 ttl=64 time=0.563 ms
64 bytes from kamiya.v6.klab.org: icmp_seq=3 ttl=64 time=0.363 ms

--- kamiya.v6.klab.org ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 20022ms
rtt min/avg/max/mdev = 0.338/0.475/0.569/0.102 ms
0.006u 0.004s 0:40.04 0.0%      0+0k 0+0io 0pf+0w

3 発 ping を打つだけなのに 40 秒もかかっています。-n オプションを 指定して、逆引きを抑制すると、

% time ping6 -nc 3 kamiya.v6.klab.org
PING kamiya.v6.klab.org(2001:c90:c1c:100e:2e0:81ff:feab:cdef) 56 data bytes
64 bytes from 2001:c90:c1c:100e:2e0:81ff:feab:cdef: icmp_seq=1 ttl=64 time=0.981 ms
64 bytes from 2001:c90:c1c:100e:2e0:81ff:feab:cdef: icmp_seq=2 ttl=64 time=0.354 ms
64 bytes from 2001:c90:c1c:100e:2e0:81ff:feab:cdef: icmp_seq=3 ttl=64 time=0.273 ms

--- kamiya.v6.klab.org ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 0.273/0.536/0.981/0.316 ms
0.003u 0.004s 0:02.02 0.0%      0+0k 0+0io 0pf+0w

などとフツーに 2 秒程度で終わるので、DNS の逆引きに問題があることが 分かります。一回の逆引きに 10 秒ほどかかっているようです。 てっきりネームサーバの設定の問題かと思ったのですが、host コマンドや dnsqr コマンドを使う限り、このような遅れは生じません。ping6 をはじめ、 glibc の getnameinfo(3) を使う場合だけ遅延が発生します。

後で気づいたのですが、この問題が起きるのは glibc 2.3 だけで glibc 2.2 では起きません (つまり kamiya とかでは正常に ping6 できる)。また、 当然ながら glibc 2.3 でも IP の逆引きは正常で、問題なのは IPv6 の逆引き だけです。

# 後述するように問題があるのは glibc 2.3.3 までで 2.3.4 は問題ありません こーいうときは tcpdump が基本だよね、つーことでパケットダンプしてみると、 getnameinfo(3) の場合は、

\[x20010c900c1c100e02e081fffeabcdef/128].ip6.arpa.

というクエリがまず飛び、10 秒ほどたってタイムアウトした後に

f.e.d.c.b.a.e.f.f.f.1.8.0.e.2.0.e.0.0.1.c.1.c.0.0.9.c.0.1.0.0.2.ip6.arpa.

というクエリが飛ぶことが分かりました。前者は見慣れない形式だったのですが、 「Binary Label」ないし「Bit-String」などと呼ばれる形式のようです (ちなみ に後者は nibble 形式)。「Binary Label」は、ドメイン名の一形式として RFC1035 で規定され、DNS での使い方が RFC2673 で決められたにもかかわらず、 ネームサーバでサポートされなかったために、RFC3363 で IPv6 の逆引きの方法 としては、使うのを断念されてしまった不幸な形式のようです。

とはいえ、「DNS で、どーして上位バイトが先にくるんだ~ 実装のこと何も 考えてないな~」と脊髄反射的に拒否したくなる、頭悪い (brain damaged) 形式なので、断念されて幸い、ということもできますね。

2002年8月に RFC3363 で正式に断念された形式なら、そのまま人々の記憶から 忘れ去られて欲しかったのですが、何を血迷ったか、glibc 2.3 は IPv6 の 逆引きで、まず「Bit-String」クエリを試し、タイムアウトしたら「nibble」 クエリを送信するという実装になっています (glibc の resolv は BIND 由来 だから?)。

少なくとも 2004年8月3日に公開された glibc 2.3.3 では Bit-String が デフォルトのままですね。一刻も早く Bit-String が使われなくなることを 願ってやみません。

願ってるだけではアレ (^^;) なので、ソースを見てみると、 glibc-2.3.3/resolv/nss_dns/dns-host.c (2003年10月26日) では

case AF_INET6:
  /* XXX Maybe we need an option to select whether to use the nibble
     or the bitfield form.  The RFC requires the bitfield form so
     we use it.  */

って書いてありますね (Maybe って書くくらいならオプション指定できるように しろよ...)。これが glibc-2.3.4/resolv/nss_dns/dns-host.c (2004年10月25日) だと

case AF_INET6:
  /* Only lookup with the byte string format if the user wants it.  */
  if (__builtin_expect (_res.options & RES_USEBSTRING, 0))
    {
      qp = stpcpy (qbuf, "\\[x");
      ...

に修正されています! つまりデフォルトでは nibble 形式に変更されたんです ね。めでたしめでたし。さっさと glibc 2.3.4 (2005年1月26日) に上げよっと。

...と書いてる間に自宅マシンで 2.3.4 を make して install してみました。 無事、getnameinfo(3) が遅延なく IPv6 の逆引きができるようになりました。 メール書いているうちに自己完結してしまったわけですが、まあ何かの参考に なるかも知れないし、IPv6 に興味を持ってくれる人が増えるといいな、 ということで tech に投げておきます。

#12237                                                          仙石 浩明
https://www.gcd.org/sengoku/             Hiroaki Sengoku <sengoku@gcd.org>
« Newer Posts