今回はHTTPなどのログを解析するツールであるAWStatsの変更をしてみます。
調べるのには苦労をしましたが一度使えるようにしてみればその後は楽なので気になる人はマージしてみるといいかもしれません。
ログがDNS名になっていないときは標準ではDNS名による行の無視ができない
AWStatsでログの行を無視するためには以下のオプションがあります。
- SkipHosts
- SkipUserAgents
- SkipFiles
- SkipReferrersBlackList
この中でログによって効果が変わってしまうものとしてSkipHostsがあります。
SkipHostsは一応正規表現が使えますが、対象のログファイルのホスト名がIPのみであるときはIPの検索しかできず、DNS名に変換されているときはDNS名で検索を行う、という特性があります。
そのため、ホスト名がIPになっている場合はDNS名による無視ができないわけですね。
これが意外と問題になるのはVPSなど遠隔地にサーバーがある場合で、http上で設定を行っているとどうしても自分(しかも基本的にプロバイダから与えられる動的IP)のアクセスログだけがたまるために他の人のアクセスが見えづらくなるという状態になってしまいます。
プロバイダから与えられる場合はIPの範囲は指定がかなり難しいですし、意味のあるDNS名に変換できることがかなりあるので、DNS名による除去を行いたいのですが・・・。
というわけでスクリプトをいろいろといじることに
AWStatsはPerlのスクリプトなのでPerlがわかっていればある程度いじることができます。
今回の目的は「DNS名による行の無視をできるようにする」と言うことですね。
スクリプトの変更はこんな感じです。AWStats 7.2のawstats.plに当てるパッチです。
--- awstats.pl.orig 2013-07-09 21:59:20.000000000 +0900 +++ awstats.pl 2013-10-09 20:00:00.000000000 +0900 @@ -375,7 +375,7 @@ %OSFamily %BrowsersFamily @SessionsRange %SessionsAverage %LangBrowserToLangAwstats %LangAWStatsToFlagAwstats %BrowsersSafariBuildToVersionHash @HostAliases @AllowAccessFromWebToFollowingAuthenticatedUsers - @DefaultFile @SkipDNSLookupFor + @DefaultFile @SkipDNSLookupFor @SkipHostNames @SkipHosts @SkipUserAgents @SkipFiles @SkipReferrers @NotPageFiles @OnlyHosts @OnlyUserAgents @OnlyFiles @OnlyUsers @URLWithQueryWithOnly @URLWithQueryWithout @@ -473,7 +473,7 @@ ); @HostAliases = @AllowAccessFromWebToFollowingAuthenticatedUsers = (); -@DefaultFile = @SkipDNSLookupFor = (); +@DefaultFile = @SkipDNSLookupFor = @SkipHostNames = (); @SkipHosts = @SkipUserAgents = @NotPageFiles = @SkipFiles = @SkipReferrers = (); @OnlyHosts = @OnlyUserAgents = @OnlyFiles = @OnlyUsers = (); @URLWithQueryWithOnly = @URLWithQueryWithout = (); @@ -1418,6 +1418,18 @@ } #------------------------------------------------------------------------------ +# Function: Check if parameter is in @SkipHostNames array +# Parameters: host @SkipHostNames (a NOT case sensitive precompiled regex array) +# Return: 0 Not found, 1 Found +#------------------------------------------------------------------------------ +sub SkipHostNames { + foreach (@SkipHostNames) { + if ( $_[0] =~ /$_/ ) { return 1; } + } + 0; # Not in @SkipHostNames +} + +#------------------------------------------------------------------------------ # Function: Check if parameter is in SkipHosts array # Parameters: host @SkipHosts (a NOT case sensitive precompiled regex array) # Return: 0 Not found, 1 Found @@ -1991,6 +2003,15 @@ } next; } + if ( $param =~ /^SkipHostNames/ ) { + @SkipHostNames = (); + foreach my $elem ( split( /\s+/, $value ) ) { + if ( $elem =~ /^REGEX\[(.*)\]$/i ) { $elem = $1; } + else { $elem = '^' . quotemeta($elem) . '$'; } + if ($elem) { push @SkipHostNames, qr/$elem/i; } + } + next; + } if ( $param =~ /^SkipHosts/ ) { @SkipHosts = (); foreach my $elem ( split( /\s+/, $value ) ) { @@ -17805,6 +17826,10 @@ 1 ); } + @SkipHostNames = &OptimizeArray( \@SkipHostNames, 1 ); + if ($Debug) { + debug( "SkipHostNames precompiled regex list is now @SkipHostNames", 1 ); + } @SkipHosts = &OptimizeArray( \@SkipHosts, 1 ); if ($Debug) { debug( "SkipHosts precompiled regex list is now @SkipHosts", 1 ); @@ -18453,6 +18478,71 @@ elsif ( @OnlyUserAgents && !&OnlyUserAgent( $field[$pos_agent] ) ) { $qualifdrop = "Dropped record (user agent '$field[$pos_agent]' not qualified by OnlyUserAgents)"; + } + + if (!$qualifdrop && $DNSLookup && @SkipHostNames) + { + my $Host = $field[$pos_host]; + my $HostResolved = ''; + do{ + my $ip = 0; + + if ( $Host =~ /$regipv4l/o ) { # IPv4 lighttpd + $Host =~ s/^::ffff://; + $ip = 4; + } + elsif ( $Host =~ /$regipv4/o ) { $ip = 4; } # IPv4 + elsif ( $Host =~ /$regipv6/o ) { $ip = 6; } # IPv6 + + last if !$ip; + + # Check in static DNS cache file + $HostResolved = $MyDNSTable{$Host}; + last if $HostResolved || $DNSLookup == 2; + + # Check in session cache (dynamic DNS cache file + session DNS cache) + $HostResolved = $TmpDNSLookup{$Host}; + if ( !$HostResolved ) { + if ( @SkipDNSLookupFor && &SkipDNSLookup($Host) ) { + $HostResolved = $TmpDNSLookup{$Host} = '*'; + } + else { + if ( $ip == 4 ) { + my $lookupresult = gethostbyaddr( pack( "C4", split( /\./, $Host ) ), AF_INET ); + if ( !$lookupresult + || $lookupresult =~ /$regipv4/o + || !IsAscii($lookupresult) ) { + $TmpDNSLookup{$Host} = $HostResolved = '*'; + } + else { + $TmpDNSLookup{$Host} = $HostResolved = $lookupresult; + } + } + elsif ( $ip == 6 ) { + if ( $PluginsLoaded{'GetResolvedIP'}{'ipv6'} ) + { + my $lookupresult = GetResolvedIP_ipv6($Host); + if ( !$lookupresult + || !IsAscii($lookupresult) ) + { + $TmpDNSLookup{$Host} = $HostResolved = '*'; + } + else { + $TmpDNSLookup{$Host} = $HostResolved = $lookupresult; + } + } + else { + $TmpDNSLookup{$Host} = $HostResolved = '*'; + } + } + else { error("Bad value vor ip"); } + } + } + } while(0); + + if($HostResolved ne '' && $HostResolved ne '*' && ( &SkipHostNames( $HostResolved ))){ + $qualifdrop = "Dropped record (hostname $HostResolved not qualified by SkipHostNames)"; + } } if ($qualifdrop) { $NbOfLinesDropped++;
この変更は
- 設定項目にSkipHostNamesを追加してここに無視したいDNS名(IPからの逆変換後)を設定する(書き方はSkipHostsと同じ)
- ここにあるDNS名の逆引きルーチンはこの後ろにあるDNS名の逆引きルーチンを名前を引くことだけを目的とした縮小版としている(デバッグメッセージがない)
- DNS名の逆引きがない、もしくはSkipHostNamesが設定されていないときは何も行わない
- DNS名の逆引きが成功したときはSkipHostNamesに該当する行は無効化される
というものです。DNS名の逆引きルーチンを前方に移植したのが特徴です。
そのため、DNS名の逆引きが二回発生するので効率そのものは余り良くなかったりします。まあ、一度引かれたDNS名はこのスクリプトの実行中はキャッシュされるのでそこまで問題になることはないでしょうし、デバッグメッセージも表示しなくても大丈夫、との判断でこんな感じになっています。
あとは設定部にSkipHostNamesを追加してやればめでたくDNS名による行の無効化ができます。
といっても非公式の拡張
どうせならこれが公式にできるようになればな~と思わなくもない人。
意外とこういう要望(IPで記述してあるログに対して逆引き後、指定したDNS名は無効とする)はあると思うのですが・・・。
ちなみにLAN内部でアクセスできるならSkipHostsの設定だけで十分に処理できますので、その辺はがんばってください。