Net_UserAgent_Mobile のバグについて

携帯のユーザエージェントから様々な情報を引き出すことのできるPEAR::Net_UserAgent_Mobile
とても便利で利用させてもらってます。
結構ながいこと地味な開発が続いて、2009年にstableとなったんだけど、まだ致命的なバグがある。

例えば下記のスクリプト

<?
require_once('Net/UserAgent/Mobile.php');
$user_agent = 'DoCoMo/2.0 N900iS(c100;TB;W24H12)';
$agent = Net_UserAgent_Mobile::factory($user_agent);
print "キャリア: ".$agent->getCarrierLongName()."\n";
print "機種:".$agent->getModel();
?>

ユーザエージェントを

$user_agent='Vodafone/1.0/V705SH (compatible; Y!J-SRD/1.0; http://help.yahoo.co.jp/help/jp/search/indexing/indexing-27.html)';

に変えると

Fatal error: Call to undefined function:  getcarrierlongname()

とエラーになってしまう。
自分で解析できないユーザーエージェントだと、無視せずにエラーを返して強制終了してしまう。
このバグは致命的。先々未知のユーザエージェントは続々と登場するわけで、その度にエラーが出ることになってしまう。

調べてみると、かなりの人が同じ症状で困っているし作者にバグ報告もされているが、その場しのぎで根本的な問題が修正がされることなくstableとなった。

対処方法

<?
require_once('Net/UserAgent/Mobile.php');
$user_agent='Vodafone/1.0/V705SH (compatible; Y!J-SRD/1.0; http://help.yahoo.co.jp/help/jp/search/indexing/indexing-27.html)';
$agent = Net_UserAgent_Mobile::factory($user_agent);
if (method_exists($agent,'isNonMobile')){
	print "キャリア: ".$agent->getCarrierLongName()."\n";
	print "機種:".$agent->getModel();
}

のように

if (method_exists($agent,'isNonMobile')){
~
}

でラップしてスルーさせればOK

はんにんさがし

数カ月前からウェブサーバのトラフィックの上がりが、ノコギリ状に増えている問題があった。
状況としては

NFSのreadが定期的に頻発している。

NFSのwriteも中長期で見ると右肩上がり

短期で見ると特徴的なノコギリ波形を示す

1時間に3~4回ペースで定期的に急激なNFS read/writeが生じている。
NFSの微々たるreadだし、放っておいてもサービスにはまったく問題ないんだけど、グラフが汚れて見辛いので原因追求。
長中期で見るとreadの回数は調子こいて右肩上がりのまま留まることを知らないので、このままエスカレートしていくのを放っておくわけにもいかない。
犯人の追跡開始。

・apacheのログ解析では不明だった
・mod_securityでPOSTを取ったが問題は見当たらなかった

そのため
・tcpdump port nfs -nxXs 6000 | grep read -C 20
・sar -n NFS 1 0
を同時に記録して解析を進めていくことにした。

・sarで30秒間ほど急激なreadが定期的に発生していることを確認。


・一時的にreadが急激に上がった時間をメモしておく
10時36分31秒
10時51分39秒
11時05分58秒
11時21分14秒
11時36分03秒

次に、上記時刻のtcpdumpのhexdumpをみてみる。
すると11時21分17秒に一意のデータである特定キーワード(メールアドレス)が確認できたので、これを鍵にすることにした。

・ファイルサーバーからこのメールアドレスを全ファイル検索

grep -r 'drfadsee3@yahoo.com' ./

その結果

バイナリー・ファイル./ユーザA/blog/db/comment.email.idxは一致しました
バイナリー・ファイル./ユーザA/blog/db/comment.dbは一致しました

とユーザAのウェブスペースにこのメールアドレスを含むファイルがあることが判明。
ユーザAのアクセスログからPOSTしている時間だけ抜き取ってみた。
するとメモした時間とアクセスログのPOSTの時間が一致した。

・apacheのログ

grep ユーザA apache_log | grep blog | grep POST
09:18:13 :31367: [gw4.winserversecure.com] 85.17.145.7 ユーザーA POST /blog/mt-tb.cgi/145 HTTP/1.0 500 -
09:20:20 :31153: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
09:35:41 :9147: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
09:50:56 :21595: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
10:05:58 :24286: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
10:20:41 :29154: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
10:36:01 :32163: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
10:51:07 :3821: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
11:05:28 :6609: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
11:20:44 :8161: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
11:35:33 :21388: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
11:51:17 :30978: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
12:06:07 :6032: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
12:21:20 :13718: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
12:37:55 :25341: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
12:51:58 :5509: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
13:07:38 :14897: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
13:23:05 :21912: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
13:38:30 :31049: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
13:53:28 :9117: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
14:08:33 :20520: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
14:11:35 :12844: [66.90.77.6] 66.90.77.6 ユーザーA POST /blog/mt-tb.cgi/81 HTTP/1.0 200 79
14:23:46 :28173: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
14:34:18 :4474: [xanhlacay.info] 74.86.238.186 ユーザーA POST /blog/mt-tb.cgi/105 HTTP/1.0 500 -
14:39:20 :8180: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
14:54:36 :19139: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
15:09:06 :30925: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
15:24:28 :7162: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
15:39:50 :17200: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
15:54:30 :29185: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -
16:10:49 :6930: [93.174.93.58] 93.174.93.58 ユーザーA POST /blog/mt-comments.cgi HTTP/1.1 500 -

原因はユーザAに定期的に訪れる掲示板SPAMのボットによるものであることがわかった。

一般的にこの手のボットからのアクセスはapacheのログ解析やmod_securityのログ解析だけで比較的簡単に発見できるものだけど、今回は特異なケースだった。
SPAMをPOSTされて500エラーを返しているにもかかわらず、mt-comments.cgiは実際に書き込みを行っているんである(もちろんコメント自体にSPAMは表示されないし、ログにも500エラーしか残らない)。
そもそも特定の変則的なメッセージのPOSTで500エラーが発生する時点でMTの不具合だろう。
他でこんな変態ポストをされたら犯人特定は極めて困難だと思う。

対策方法

・GeoIPでアクセス規制を入れる

GeoIPEnable On
Order deny,allow
deny from all
SetEnvIf GEOIP_COUNTRY_CODE JP AllowCountry
Allow from .googlebot.com
Allow from .yahoo.net
Allow from .msn.com
Allow from .naver.jp
Allow from env=AllowCountry

結果、下記のように問題は解決した。



BINDエラーチェック dnswalk

bindは標準で
named-checkzone
named-checkconf
などのエラーチェックツールが付属してくる。
でもこれはゾーンファイルやコンフィグファイルにsyntaxなエラーがあるかチェックするだけ。
DNSはけっこうテキトーな設定でもとりあえず動いてしまい、infoレベルのsyslogにはエラーを吐かないことも多いので、いま一歩詳細なチェックツールを使っておいたほうがいい。
dnswalk
http://sourceforge.jp/projects/sfnet_dnswalk/

このtarボールは直下に解凍されてしまうので、ディレクトリを掘って解凍すること。

Net::DNSが必要

cpan install Net::DNS

perlスクリプトなのでそのまま実行可能。ただperlへのパスが奇異なのでそのままでは動かない。

vi dnswalk
#!/usr/contrib/bin/perl
     ↓
#!/usr/bin/perl
chown root.root dnswalk
cp dnswalk /usr/bin
# dnswalk example.com.
Checking example.com.
BAD: SOA record not found for example.com.
BAD: example.com. has NO authoritative nameservers!
BAD: All zone transfer attempts of example.com. failed!
0 failures, 0 warnings, 3 errors.

mod_vhost_alias が普及しない理由

バーチャルホストが多くなってくると、同じ設定を繰り返すことが無駄に思えて自動化・構造化したくなってくるのが人の情というもの。
バーチャルホストを動的に割り当てる方法には、mod_rewrite で強制的に割り当てる方法と、mod_vhost_alias を使う方法があるけれども、返す環境変数がそれぞれ異なってしまう問題がある。

http://sub.example.com  を /home/exmaple.com/plublic_html/sub/ に割り当てる際のバーチャルホスト設定として、下記の3種の方法を試し、その際に返すDOCUMENT_ROOTを取得してみた。

(1) VirtualHost

<VirtualHost *>
DocumentRoot /home/example.com/public_html/sub/
ServerName sub.exmaple.com
</VirtualHost>

結果

/home/example.com/public_html/sub/

(2) mod_rewrite

<VirtualHost *>
DocumentRoot /home/example.com/public_html/
ServerName example.com
ServerAlias *.example.com
RewriteEngine On
RewriteCond %{HTTP_HOST} ^[^.]+\.example\.com$
RewriteRule (.*) %{HTTP_HOST}$1 [C]
RewriteRule ^([^.]+)\.example\.com(.*) /home/example\.com/public_html/$1/$2 [L]
</VirtualHost>

結果

/home/example.com/public_html/

(3) mod_vhost_alias

UseCanonicalName Off
VirtualDocumentRoot /home/%2+/public_html/%1

結果

/home

以上のように、(2)または(3)の設定で、DOCUMENT_ROOTを、(1)の状態で扱うことはできない。
この不具合によって、環境変数を参照するCGIやモジュールに正常な動作は期待できない。

mod_vhost_alias のこの不具合はbugzillaで報告されているが、解消される様子はない。
https://issues.apache.org/bugzilla/show_bug.cgi?id=26052

具体的には下記のように環境変数が異なる。
■vhost

<VirtualHost *>
        DocumentRoot /var/www/html/exmaple.com/public_html/
        ServerName exmaple.com
</VirtualHost>
DOCUMENT_ROOT="/var/www/html/exmaple.com/public_html/sub/"
GATEWAY_INTERFACE="CGI/1.1"
HTTP_ACCEPT="text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
HTTP_ACCEPT_CHARSET="Shift_JIS,utf-8;q=0.7,*;q=0.7"
HTTP_ACCEPT_ENCODING="gzip,deflate"
HTTP_ACCEPT_LANGUAGE="ja,en-us;q=0.7,en;q=0.3"
HTTP_CONNECTION="keep-alive"
HTTP_HOST="sub.exmaple.com"
HTTP_KEEP_ALIVE="300"
HTTP_REFERER="http://sub.exmaple.com/"
HTTP_USER_AGENT="Mozilla/5.0 (Windows; U; Windows NT 6.1; ja; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)"
PATH="/bin"
QUERY_STRING=""
REMOTE_ADDR="192.168.10.2"
REMOTE_PORT="65356"
REQUEST_METHOD="GET"
REQUEST_URI="/printenv.cgi"
SCRIPT_FILENAME="/var/www/html/exmaple.com/public_html/sub/printenv.cgi"
SCRIPT_NAME="/printenv.cgi"
SERVER_ADDR="192.168.0.2"
SERVER_ADMIN="admin@exmaple.com"
SERVER_NAME="sub.exmaple.com"
SERVER_PORT="80"
SERVER_PROTOCOL="HTTP/1.1"
SERVER_SIGNATURE=""
SERVER_SOFTWARE="Apache/2.2.14 (Unix)"

■rewrite

<VirtualHost *>
        DocumentRoot /var/www/html/exmaple.com/public_html/
        ServerName exmaple.com
        ServerAlias *.exmaple.com
RewriteEngine On
RewriteCond   %{HTTP_HOST}                 ^[^.]+\.example\.com$
RewriteRule   (.*)                         %{HTTP_HOST}$1             [C]
RewriteRule   ^([^.]+)\.example\.com(.*)  /var/www/html/example\.com/public_html/$1/$2      [L]
</VirtualHost>
DOCUMENT_ROOT="/var/www/html/exmaple.com/public_html/"
GATEWAY_INTERFACE="CGI/1.1"
HTTP_ACCEPT="text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
HTTP_ACCEPT_CHARSET="Shift_JIS,utf-8;q=0.7,*;q=0.7"
HTTP_ACCEPT_ENCODING="gzip,deflate"
HTTP_ACCEPT_LANGUAGE="ja,en-us;q=0.7,en;q=0.3"
HTTP_CONNECTION="keep-alive"
HTTP_HOST="sub.exmaple.com"
HTTP_KEEP_ALIVE="300"
HTTP_REFERER="http://sub.exmaple.com/"
HTTP_USER_AGENT="Mozilla/5.0 (Windows; U; Windows NT 6.1; ja; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)"
PATH="/bin"
QUERY_STRING=""
REMOTE_ADDR="192.168.10.2"
REMOTE_PORT="65352"
REQUEST_METHOD="GET"
REQUEST_URI="/printenv.cgi"
SCRIPT_FILENAME="/var/www/html/exmaple.com/public_html/sub/printenv.cgi"
SCRIPT_NAME="/sub/printenv.cgi"
SCRIPT_URI="http://sub.exmaple.com/printenv.cgi"
SCRIPT_URL="/printenv.cgi"
SERVER_ADDR="192.168.0.2"
SERVER_ADMIN="admin@exmaple.com"
SERVER_NAME="sub.exmaple.com"
SERVER_PORT="80"
SERVER_PROTOCOL="HTTP/1.1"
SERVER_SIGNATURE=""
SERVER_SOFTWARE="Apache/2.2.14 (Unix)"

■mod_vhost_alias

UseCanonicalName    Off
VirtualDocumentRoot /var/www/html/%2+/public_html/%1
DOCUMENT_ROOT="/var/www/html"
GATEWAY_INTERFACE="CGI/1.1"
HTTP_ACCEPT="text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
HTTP_ACCEPT_CHARSET="Shift_JIS,utf-8;q=0.7,*;q=0.7"
HTTP_ACCEPT_ENCODING="gzip,deflate"
HTTP_ACCEPT_LANGUAGE="ja,en-us;q=0.7,en;q=0.3"
HTTP_CONNECTION="keep-alive"
HTTP_HOST="sub.exmaple.com"
HTTP_KEEP_ALIVE="300"
HTTP_REFERER="http://sub.exmaple.com/"
HTTP_USER_AGENT="Mozilla/5.0 (Windows; U; Windows NT 6.1; ja; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)"
PATH="/bin"
QUERY_STRING=""
REMOTE_ADDR="192.168.10.2"
REMOTE_PORT="65512"
REQUEST_METHOD="GET"
REQUEST_URI="/printenv.cgi"
SCRIPT_FILENAME="/var/www/html/exmaple.com/public_html/sub/printenv.cgi"
SCRIPT_NAME="/printenv.cgi"
SERVER_ADDR="192.168.0.2"
SERVER_ADMIN="admin@exmaple.com"
SERVER_NAME="sub.exmaple.com"
SERVER_PORT="80"
SERVER_PROTOCOL="HTTP/1.1"
SERVER_SIGNATURE=""
SERVER_SOFTWARE="Apache/2.2.14 (Unix)"

net2ftpのバグについて

net2ftp バージョン0.98にはSafariやIEでアクセスした際にダウンロードするファイル名がすべてindex.phpになってしまうバグがある。
この問題は下記でも指摘されているが修正されていない。
http://www.net2ftp.org/forums/viewtopic.php?id=2960

includes/filesystem.inc.php
を下記に修正

	header("Content-Type: " . $content_type);
	header("Expires: " . gmdate("D, d M Y H:i:s") . " GMT");
	if ($net2ftp_globals["browser_agent"] == "IE") {
		header("Content-Disposition: $content_disposition; filename=\"" . $filename_html . "\"");
		header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
		header("Pragma: public");
	}
//if ($net2ftp_globals["browser_platform"] == "Mac"){
if ($net2ftp_globals["browser_agent"] == "Safari" || $net2ftp_globals["browser_agent"] == "IE"){
    header("Content-type: application/force-download");
    header("Content-Transfer-Encoding: Binary");
    header("Content-disposition: attachment; filename=\"".$filename_html."\"");
    header("Content-Description: $filename_html");
    header("Content-Length: $filesize");
    header("Connection: close");
    } else {
		// Firefox needs an asterisk to enable filenames with special characters
		header("Content-Disposition: $content_disposition; filename*=\"" . $filename_html . "\"");
		header("Pragma: no-cache");
	}

	header("Content-Description: $filename_html");
	header("Content-Length: $filesize");
	header("Connection: close");

} // End function sendDownloadHeaders

フォーラムでは

if ($net2ftp_globals["browser_platform"] == "Mac"){

としているけど、WindowsのSafariでも再現するどころか、IE7、IE8でも再現するので

if ($net2ftp_globals["browser_agent"] == "Safari" || $net2ftp_globals["browser_agent"] == "IE"){

とした。

ブログ開始

本日から鯖缶日誌を開始します。