まだ重たいCMSをお使いですか?
毎秒1000リクエスト を捌く超高速CMS「adiary

2008/02/08(金)2行に渡るメールヘッダの正しい処理

問題発生

ある方より、メール投稿利用時に長い日本語タイトルを付けると、途中に半角スペースが入ってしまうというバグ報告を受けました。

Subject: =?ISO-2022-JP?B?GyRCPmFHKz5hRys+YUcrPmFHKz5hRys+YUcrPmFHKz5hGyhC?=
 =?ISO-2022-JP?B?GyRCRys+YUcrPmFHKz5hRys+YUcrPmFHKz5hRys+YUcrPmFHKz5hGyhC?=

のようにMIMEエンコードが長くなり空白が入ったときに、この空白がデコードされてもそのまま残ってしまうことが原因です。やっつけならこの空白を除去するだけで良いのですが、やっつけプログラムは最低なのできちんと調査してみました。

長いメールヘッダの規定

RFC 2822「Internet Message Format」には次のように規定されています。

2.1.1. 行の長さの制限

それぞれの行の文字はCRLFを除いて、決して998文字以下でなければならず(MUST)、78文字以下であるべきである(SHOULD)。

2.2.3. 長いヘッダフィールド

それぞれのヘッダフィールドは(略)単一論理行である。しかしながら便宜のため、文字数制限のために、ヘッダフィールドの一部であるフィールドボディは複数行に分割して表現でき、これをfoldingと呼ぶ。どこであれfoldingのホワイトスペース(WSPではない)があるとき、WSP(SPACE/0x20またはTAB/0x09)の前にCRLFを挿入してよい。例えば、ヘッダフィールド:

Subject: This is a test

は以下のように表現できる。

Subject: This
 is a test

(中略)

このfoldingされたヘッダフィールドの複数行表現を1行表現にする過程をunfolding と呼ぶ。unfolding はWSPがすぐ後に続くあらゆるCRLFを単に削除することでなされる。

つまりRFCの規定に従い、最初に例示したメールヘッダは次のように復元されます。

Subject: =?ISO-2022-JP?B?(略)?= =?ISO-2022-JP?B?(略)?=

ここまでは正しい復元過程であり、やっつけ実装として考えた「単純に複数行ヘッダの2行目以降の先頭空白の除去」は誤りであることがわかります。実際、長い英字タイトルに1文字だけ空白を入れた「aa(略)aaa bbb(略)」というメールを送信したところ

Subject: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaa  ←※実際にはここまで1行
 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

というように78文字の制限を超えてメールヘッダが作られました。単純な先頭空白除去では、このような改行位置が半角の文になっているメールを受信した際に問題が発生します*1。そもそもRFCの規定で「CRLFを単に削除する」となっているのですから、そこは正しく従わねばなりません。

*1 : ちなはにnPOPはこの実装でした。

MIMEの規定

では「長い日本語タイトルの途中に空白が入る問題」はどこに実装の誤りがあるのでしょうか。これについて、RFC 2047「非ASCIIテクストのためのメッセージヘッダ拡張」を参照します。

6.2. 'encoded-word' のディスプレイ

複数の 'encoded-word' を含むある特定のヘッダフィールドを表示する時は、隣接する 'encoded-word' を分離するあらゆる 'linear-white-space' は無視される。

'encoded-word'は、いわゆるMIMEエンコードのことです。linear-white-space はRFC 822により次にように定義されています。

HTAB        =  <ASCII HT, horizontal-tab>   ; (文字コード:9)
LWSP-char   =  SPACE / HTAB                 ; semantics = SPACE
linear-white-space =  1*([CRLF] LWSP-char)  ; semantics = SPACE

[CRLF] は CRLF があってもなくても良い。(0回以上1回未満の繰り返し)
1*(element) はelementの少なくとも1つ以上の繰り返し。

まとめると「=?ISO-2022-JP?B?(略)?=」ブロックと「=?ISO-2022-JP?B?(略)?=」ブロックの間にある、"CR LF SPACE(又はTAB)"の1つ以上の繰り返しは削除すべしということになります。

正しいメールヘッダ復元の実装

Perl正規表現で次にように行います。

$hl = (1つのヘッダフィールド(1行または複数行))
while ($hl =~ /(.*?=\?ISO-2022-JP\?B\?[A-Za-z0-9\+\/=]*\?=)(?:(?:\r\n)?[\t ])+(=\?ISO-2022-JP\?B\?[A-Za-z0-9\+\/=]*\?=.*)/i) {
	$hl="$1$2";
}
$hl =~ s/=\?ISO-2022-JP\?B\?([A-Za-z0-9\+\/=]*)\?=/ &ベース64デコード($1) /ieg;

ベース64デコード関数は別に用意してください。(たとえばこれ

(?:(?:\r\n)?\r\n[\t ])+

上記が、linear-white-spaceに相当します。ただ、メールデータを読み込む状況によっては \n \r \r\n のいずれになってプログラムに渡されるかわかりませんので、若干厳密な実装ではなくなりますが、

$hl =~ s/\r\n|\r/\n/g;

しておいてから、

(?:\n?[\t ])+

の方が良いと思います。さらに言えば "RFC 2822" の規定を先に適用して、先に改行コードを除去してから、MIMEエンコード間の空白除去を行っても動作としては変わりません。それをベースにした adiary の実装を最後に示します。

$hl =~ s/\x00//g;
my @buf;
# MIMEデコードをしてエスケープ表記"\0 num \0"に置き換え
$hl =~ s/=\?[\w\-]+\?B\?([A-Za-z0-9\+\/=]*)\?=/
	push(@buf, $self->base64decode($1));
	"\x00$#buf\x00";
/ieg;
# RFC 2047
$hl =~ s/\x00[\t ]+\x00/\x00\x00/g;
# エスケープ復元
$hl =~ s/\x00(\d+)\x00/$buf[$1]/g;

2008/02/05(火)skltonシステムで再帰関数

最近書いた、adiary関連のとあるskeltonで必要に迫られて再帰関数を作りました。

再帰関数なスケルトン

<@local(ary, subdir, prefix, depth, header, idx, x)>
<@>再帰サブルーチン --------------------------------------------
<$sub = begin)>
<$subdir = argv#1>
<$prefix = argv#2>
<$depth  = argv#3>
<$header = if(depth, '<img width=' . (9*depth) . #' height="9" src="<@Basepath><@v.album_icons>spacer.gif">')>
<$idx = 0>
<@forexec(t, argv#0, begin)>
	<$push(dirlist, t)>
	<$ary = v.search_folderlist(basedir, t.path, t.pathname)>
	<$uri_encode(x = t.path)><@>URIエンコード済パス / ""内に書いても安全にする
	<@ifexec(ary#0, begin, begin)>
<@header><a href="#" onClick="return toggleBlock('<@prefix><@idx>');">
<img src="<@Basepath><@v.album_icons>minus.png" id="img_<@prefix><@idx>"></a>
-<a href="<@v.myself2><@x>"><@t.name></a><br>
<div id="blk_<@prefix><@idx>"."_">
<@exec(sub, ary, "<@t.path>", "<@prefix><@idx>_", depth+1)>
</div>
	<$else>
<@header>+-<a href="<@v.myself2><@x>"><@t.name></a><br>
	<$end>
<$idx+=1>
<$end>
<$end>
<@>main ---------------------------------------------------------
<$dirlist = argv#0>
<$basedir = v.user_dir>
<@exec(sub, v.search_folderlist(v.user_dir), "", "blk_", 0)>

ローカル変数宣言に再起関数。ここまでくるとすでにスケルトンではないような気もしますが、viewに関連する部分はプログラム本体には入れたくないので、こんな感じです。

このとおり Satsuki-system はかなり強力ですが、もう少し記述能力をアップさせたいところではあります。

<@x=forexec(t, v.load_folderlist(), begin)>

とかできませんし、ローカル変数宣言もファイル全体に対する宣言となっているにも関わらず、実装は関数単位分離になるため、かなり理解しにくいところがあります。*1

*1 : <$begin>~<$end>構文には、関数として展開される場合と埋め込みとして直接展開されるパターンがある。元々は関数展開しかなっかたものを、高速化のため部分的に直接展開を取り入れたことが複雑化の原因。

スケルトンの実行コード

ついでに。このスケルトンは次のような実行コード(Perl)にコンパイルされた後、__cache/ ディレクトリ以下に保存されます。

sub {
	my $Out = shift;
	my $R   = shift;
	my $Ary = shift;
	my $v   = $R->{v};
	$_[1] = \$v;
	my ($x,$idx,$depth,$header,$prefix,$subdir,$ary);
	$R->{sub} = $Ary->[1];
	$R->{dirlist} = $R->{argv}->[0];
	$R->{basedir} = $v->{user_dir};
	$_[0]=38; push(@$Out, $R->exec($R->{sub},$v->search_folderlist($v->{user_dir}),"","blk_",0)); $R->{Break} && return;
	return;
}
sub {
	my $Out = shift;
	my $R   = shift;
	my $Ary = shift;
	my $v   = $R->{v};
	$_[1] = \$v;
	my ($x,$idx,$depth,$header,$prefix,$subdir,$ary);
	my $subdir = $R->{argv}->[1];
	my $prefix = $R->{argv}->[2];
	my $depth = $R->{argv}->[3];
	my $header = ($depth != 0 && (('<img width=' . (9 * $depth)) . " height=\"9\" src=\"$R->{Basepath}$v->{album_icons}spacer.gif\">"));
	my $idx = 0;
	my $X=$R->{argv}->[0];
	if (ref($X) ne 'ARRAY') { $X=[]; $R->error_from("line 17 at $R->{__src_file}", '[executor] forexec: data is not array'); };
	foreach(@{ $X }) { $R->{t}=$_;
		$_[0]=18; push(@{$R->{dirlist}},$R->{t});
		$_[0]=19; my $ary = $v->search_folderlist($R->{basedir},$R->{t}->{path},$R->{t}->{pathname}); $R->{Break} && return;
		$_[0]=20; $R->uri_encode(($x = $R->{t}->{path})); $R->{Break} && return;
		if ($ary->[0]) {
			push(@$Out, $header);
			push(@$Out, '<a href="#" onClick="return toggleBlock(\'');
			push(@$Out, $prefix);
			push(@$Out, $idx);
			push(@$Out, '\');"><img src="');
			push(@$Out, $R->{Basepath});
			push(@$Out, $v->{album_icons});
			push(@$Out, 'minus.png" id="img_');
			push(@$Out, $prefix);
			push(@$Out, $idx);
			push(@$Out, '"></a>-<a href="');
			push(@$Out, $v->{myself2});
			push(@$Out, $x);
			push(@$Out, '">');
			push(@$Out, $R->{t}->{name});
			push(@$Out, '</a><br>
<div id="blk_');
			push(@$Out, $prefix);
			push(@$Out, $idx);
			push(@$Out, '_">
');
			$_[0]=24; push(@$Out, $R->exec($R->{sub},$ary,"$R->{t}->{path}",("$prefix$idx" . "_"),($depth + 1))); $R->{Break} && return;
			push(@$Out, '</div>
');
		} else {
			push(@$Out, $header);
			push(@$Out, '+-<a href="');
			push(@$Out, $v->{myself2});
			push(@$Out, $x);
			push(@$Out, '">');
			push(@$Out, $R->{t}->{name});
			push(@$Out, '</a><br>
');
		}
		$idx += 1;
	}
	return;
}

これはコンパイルログディレクトリ <$Compile_log_dir="xxx"> を設定することで誰でもみることが出来ます。*2

なお出力を文字列連結せずスタックに積んでいくのは、高速化のためです。(コンパイラが)無駄に綺麗なソースを出力するのは単なる趣味です(笑)

*2 : すでにキャッシュされているスケルトンはコンパイルの必要がないためログは出力されません。キャッシュを消すか何かしてください。

2008/02/03(日)SpeedyCGI/FastCGI でモジュール動的更新

Satsuki-system(adiaryの基礎)で使用している、モジュールの自動更新機構と SpeedyCGI/FastCGI での実装について詳細に解説します。

モジュール自動更新の必要性

SpeedyCGIやmod_perl等々のスクリプトキャッシュ環境で実行速度が改善するのは、プログラム本体やライブラリをコンパイルした状態で内部に保持するためスクリプト実行時間の大半を占めるコンパイル時間が短縮されるというメリットがあるためです。特に最近のスクリプトは便利なライブラリを使用する傾向が強く、実行時間の95%以上がスクリプトのコンパイル時間なんてこともめずらしくありません。*1

しかしそのような環境では、スクリプトライブラリを書き換えてもプログラムは依然としてキャッシュした古いライブラリを使用し続けるので、少し書き換える度に SpeedyCGI の常駐プロセスを手動で殺したり、Apacheを再起動させたりといった手間が必要になります。

これを避けるためにはスクリプト自身でライブラリの更新をチェックし、ライブラリが更新されていた際に自動的にスクリプトを再起動(リロード)させる仕組みが必要になります。

*1 : adiaryはPerlの付属ライブラリを極力使用せず、車輪を再発明しまくることによって実行速度を向上させています。

ライブラリ更新チェック方法

Satsuki-system(adiary) では lib/Satsuki/Base_auto2.pm にその仕組みが実装されています(むしろそのファイルは、それ関連しか書かれてません)。

# AGPLv3
#######################################################
# ■SpeedyCGI / FastCGI用のリロードチェックサブルーチン
#######################################################
my %lib_modtime;
#------------------------------------------------------
# ●新たにロードされたライブラリの更新時間を保存
#------------------------------------------------------
sub save_lib_modtime {
	while (my ($pkg, $file) = each(%INC)) {
		if (exists $lib_modtime{$file}) { next; }
		$lib_modtime{$file} = (stat($file)) [9];
	}
}
#------------------------------------------------------
# ●ライブラリが更新されているか確認
#------------------------------------------------------
sub check_lib_update {
	while(my ($file, $current_tm) = each(%lib_modtime)) {
		my $tm = (stat($file)) [9];
		if ($tm > $current_tm) {
			return 1;	# 更新されている
		}
	}
	return 0;
}

%lib_modtime がライブラリの更新時刻を保持するハッシュです。save_lib_modtime をプログラムの終了時に呼び出し、新たにロードされたライブラリを %lib_modtime に保存しています。そしてプログラム開始時に check_lib_update を呼び出して、更新されたライブラリがあるか確認しています。

もし更新されたライブラリが発見された場合は、この実行終了後に(キャッシュされた)プロセスを終了する予約をして、リクエストされたURIにリダイレクトします。*2

#SpeedyCGI
if ($self->check_lib_update()) {
	CGI::SpeedyCGI->shutdown_next_time();	# リクエスト終了後シャットダウン
	if ($ENV{REQUEST_METHOD} ne 'POST') {	# POSTはリダイレクトできないのでそのまま処理
		$self->redirect($ENV{REQUEST_URI});	# 同一URLにリダイレクト
	}
}

FastCGIでの処理も同様です。mod_perlの場合はApache自体を reload させるわけにはいかないので、別の方法になっています。

*2 : 但しPOSTメソッド時を除く

なぜか更新が検出できない

SpeedyCGI のバグなのか仕様なのか、Base.pm の更新が検出できない(リロードされない)というバグに長いこと悩まされていました。SpeedyCGI は何分かアクセスがなければ自動でプロセスが落ちるためさほど深刻ではありませんが、奇妙なので少し調査してみました。

SpeedyCGIの起動ルーチンは次のようになっています。

#!/usr/bin/speedy
use Satsuki::Base();
{
	# ルートオブジェクト生成
	my $ROBJ = Satsuki::Base->new();
	# SpeedyCGI向けリロードチェック
	$ROBJ->init_for_speedycgi('Satsuki::');
	# メイン
	$ROBJ->start_up();
}

色々調べてみると、CGI::SpeedyCGI->shutdown_next_time(); 自体の処理は問題なく、SpeedyCGI自体はリロードがかかっているにも関わらず、Satsuki::Base がリロードされないということが判明しました。どうやらshutdown_next_time() は本当にリロードしているわけではなく、use段階は再実行しない(perlのBEGIN構文)ようです*3

試行錯誤の末、use Satsuki::Base(); の部分を次のように書き換えることで解決されました。

if (! $Satsuki::Base::VERSION) {
	delete $INC{"Satsuki/Base.pm"};
	require Satsuki::Base;
}

ここに delete が付いている理由ですが、Base.pm のロードの途中に構文エラーなどで失敗した場合、requireしたライブラリにロード済の情報が残ったまま($INC{"Satsuki/Base.pm"}が保存されたまま)の状態でキャッシュしてしまいます。この状態ではライブラリのリロードに問題があるため、ロード情報を消してから require します。

*3 : 注:cgiファイルを書き換えた場合は、useも含めて再実行されます。

2008/01/24(木)adiary 1.44のパッチ集3

adiary 1.44のパッチ集3です。前回のパッチ集の内容を含んでいます。

パッチ集

ここから落としてください

  • 【fix】メール投稿時の拡張子チェックで、小文字拡張子決めうちだった点(大文字拡張子だと投稿できない問題)を修正しました。
  • 【fix】はてなスターがつけられない問題を修正しました。(Thanks to ひとぅ)
  • 【fix】印刷用の表示で画像が表示されない不具合を修正しました。([ml:users:162:Thanks to 小沼])
  • 【fix】IE使用時「URLの自動リンク」の設定が無効になっていた不具合を修正しました。(Thanks to 浅野)
  • 【fix】埋め込みテキスト編集で、<@s.bodyend_1st>の設定値が展開されて保存される不具合を修正しました。(Thanks to 浅野)

for Ver1.990(C73)

ここにおきました(EUC-JP)。http://adiary.org/download/patch/v1.990/

2008/01/21(月)adiary 1.44のパッチ集2

adiary 1.44のパッチ集2です。前回のパッチ集の内容を含んでいます。

パッチ集

ここから落としてください

  • 【fix】はてなスターがつけられない問題を修正しました。(Thanks to ひとぅ)
  • 【fix】印刷用の表示で画像が表示されない不具合を修正しました。([ml:users:162:Thanks to 小沼])
  • 【fix】IE使用時「URLの自動リンク」の設定が無効になっていた不具合を修正しました。(Thanks to 浅野)
  • 【fix】埋め込みテキスト編集で、<@s.bodyend_1st>の設定値が展開されて保存される不具合を修正しました。(Thanks to 浅野)

前回のパッチ集ではUTF-8版を提供し忘れていました(汗

for Ver1.990(C73)

ここにおきました(EUC-JP)。http://adiary.org/download/patch/v1.990/