最終更新: 2014-12-05T17:23+0900
送信元が限られてるのかワンパターンだったので、これまでは </a> を NGワードにしてほとんどの spamを防いでいたんだけど、今日は手ひどくやられた。
フィルタはインストール方法がよくわからない。Comment-key Filterを tdiary/filter/ に設置したがエラーが出るので、とりあえず最新の 2.3.3.20091124にアップデートした。そうすると plugin/60sf.rbが有効になっているはずなので misc/filter/ にフィルタスクリプトを、misc/filter/plugin/ にフィルタに関連する設定などを表示するプラグインスクリプトを、misc/filter/plugin/ja/ などにプラグインの言語リソースをインストールし、設定画面でフィルタを有効にするといずれのスクリプトも読み込まれるようになる。
コメントフォームにキー文字列を埋め込み、コメントが HTMLフォームを通して投稿されたことを確認する。キーは日記設置者の決めた文字列と日記の日付によって一意に決められるので、ある日のキー文字列を一度取得すればその日には何件でも機械的に投稿することができる。GETして HTMLを切り刻む手間をかければ最初から機械的に投稿することもできる。そんなのは防げない。
tDiary-2.3.3.20091124にインストールするのなら key.rbを misc/filter/key.rbへ、comment_key.rbを misc/filter/plugin/key.rbへ、ja/comment_key.rbを misc/filter/plugin/ja/key.rbへコピーすれば良い。key.rbというファイル名と KeyFilterというクラス名は対応しているので、key.rbから comment_key.rbへの改名はやめたほうがいい。コピー後にフィルタを有効にし、キー文字列を設定する。
連投対策。同じ IPアドレスからの連続する投稿をはじく。30分につき 3件までしか許可しない、など。
# coding: utf-8 # # limit_freq.rb: # # a spam-filtering plugin of tDiary # rejects frequent comments posted from an IP address. module TDiary::Filter class LimitFreqFilter < Filter def comment_filter( diary, comment ) now = Time.now.to_i comment_i = Comment_t.new( now, @cgi.remote_addr ) # 数値形式の日時と文字列形式の IPアドレスの二要素 # 配列の配列を、日時をもとに昇順にソートしたもの。 # log = [[123456, "1.2.3.4"], [234567, "5.6.7.8"],...] log = [] require 'pstore' ps = PStore.new( cache_path ) ps.transaction( false ) { |db| log = db.fetch( DBRoot, log ) # 新しいコメントを logに追加する。 log.insert( ArrayExtension.lower_bound(log){|pair| pair.first - comment_i.time }, [comment_i.time, comment_i.ipaddr] ) # 古い logを捨てる。 oldest = now - time_span log.slice!( 0 ... ArrayExtension.lower_bound(log){|pair| pair.first - oldest } ) db[DBRoot] = log db.commit } # 残った logに投稿者の IPアドレスがいくつ含まれるか数え、 # その数が閾値を超えていないか確かめる。 count = 0 if log.all?{|pair| count += 1 if pair.last == comment_i.ipaddr count < threshold } then return this_is_not_a_spam( comment ) else debug("limit_freq.rb: spam: the # of comments posted from #{comment_i.ipaddr} exceeds threshold:#{threshold} within the last #{time_span.to_i}seconds.") return this_is_a_spam( comment ) end rescue debug("limit_freq.rb: BUG: #{$!}") return this_is_not_a_spam( comment ) end private DBRoot = 'LimitFreqLog' Comment_t = Struct.new(:time, :ipaddr) def cache_path return File.join( (@conf.cache_path || "#{@conf.data_path}cache"), 'limit_freq.data' ) end # どれだけの期間(秒単位)、コメントの投稿頻度の判定に # 利用するログ(日時とIPアドレスのペア)を保存するか。 def time_span 30 * 60 end # time_span当たり、何件目の投稿からを拒否するか。 def threshold 4 end module ArrayExtension # ソート済みだという前提を活かしていない! def lower_bound( arr, &block ) index = 0 arr.length.times{ return index if 0 <= yield( arr[index] ) index += 1 } return index end module_function :lower_bound end end end
やめればいいのに本体もちょろちょろと変更。
Index: core/tdiary.rb =================================================================== --- core/tdiary.rb (リビジョン 44436) +++ core/tdiary.rb (作業コピー) @@ -342,6 +342,22 @@ @logger.info("#{@cgi.remote_addr}->#{(@cgi.params['date'][0] || 'no date').dump}: #{msg}") end + + private + # config: hide or drop spam comment + def hide_spam? + return @conf.options.include?('spamfilter.filter_mode') && @conf.options['spamfilter.filter_mode'] + end + + def this_is_a_spam( comment ) + comment.show = false + return hide_spam? + rescue + return false # comment could be a String(@cgi.referer). + end + def this_is_not_a_spam( comment ) + return true + end end end @@ -1303,7 +1319,7 @@ filter_path = @conf.filter_path || "#{PATH}/tdiary/filter" Dir::glob( "#{filter_path}/*.rb" ).sort.each do |file| require file.untaint - @filters << TDiary::Filter::const_get( "#{File::basename( file, '.rb' ).capitalize}Filter" )::new( @cgi, @conf, @logger ) + @filters << TDiary::Filter::const_get( "#{File::basename( file, '.rb' ).split(/[-_]+/).map{|x|x.capitalize}.join('')}Filter" )::new( @cgi, @conf, @logger ) end end Index: core/plugin/60sf.rb =================================================================== --- core/plugin/60sf.rb (リビジョン 44436) +++ core/plugin/60sf.rb (作業コピー) @@ -4,10 +4,9 @@ # Modified by KURODA Hiraku. SF_PREFIX = 'sf' -@sf_path = ( @conf["#{SF_PREFIX}.path"] || "#{::TDiary::PATH}/misc/filter" ).to_a -@sf_path = @sf_path.collect do |path| - /\/$/ =~ path ? path.chop : path -end +@sf_path = ( @conf["#{SF_PREFIX}.path"] || "#{::TDiary::PATH}/misc/filter" ).to_a.map{|path| + path.chomp('/') +} # get plugin option def sf_option( key ) @@ -128,7 +127,7 @@ if File.readable?( path ) then begin require path - @sf_filters << TDiary::Filter::const_get("#{File::basename(filename, ".rb").capitalize}Filter")::new(@cgi, @conf, @logger) + @sf_filters << ::TDiary::Filter::const_get("#{File::basename(filename, ".rb").split(/[-_]+/).map{|x|x.capitalize}.join('')}Filter")::new(@cgi, @conf, @logger) plugin_path = "#{dir}/plugin/#{filename}" load_plugin(plugin_path) if File.readable?(plugin_path) rescue Exception
一件二件すり抜けるぐらいはいいかと思っていたがここ数日は毎日なのでいいかげん腹が立つ。なによりアクセスログを見たらヒット数、転送量トップが spammerだというのが決定的に許せない。節度を知れ。iptablesはいじれないので .htaccessで Apacheに拒否してもらう。
spamコメントがくる月っていうのは、いつもの決まった 2、3か国からのアクセスではなく 10近い国から少数ずつアクセスがあるもんだけど、日単位では固定の IPアドレスから数十件の spamがくるのが常だった。でも今日は一件一件みごとに IPアドレスが異なっている。IPアドレスブロックで次のステージに進んでしまったのか。
spamコメントの目的が特定の URLへの誘導であるかぎりは、コメントに含まれる URLのドメインが白か黒かを判定する方法が有効でしょうね。外部に問い合わせるのは避けたかったんだけど、spamコメントの内容が URLも含めてワンパターンだから実際の問い合わせはごくごく限られた回数にできそうだし、悪くないかな。
ここで上記の comment-keyフィルタも含めてスパムフィルタの最新版が管理されていた。 >http://coderepos.org/share/browser/platform/tdiary/filter
plugin/00default.rbに含まれるメソッド navi_itemを自分でも使っていたのだけど、tDiaryをアップデートしたら三番目の引数が真偽値からリンクの rel属性文字列へと変更されているせいで rel="true" なるリンクができていた。こうする。
def navi_item( link, label, rel = nil ) rel = "nofollow" if rel == true # backward compatibility
スパムコメント一掃のために YYYYMM.tdcを削除したけど日記の編集画面からコメントが消えない。YYYYMM.parserを開くとコメントが含まれていたし、これを削除すると編集画面からもコメントが消えた。原因はデータファイルを直接削除するというイレギュラーな操作だけど、日記の編集はマスターデータに対して行いたい気もする。