2010-03-22 08:33:44 +01:00
# File: AntiFlood.pm
2010-03-24 07:47:40 +01:00
# Author: pragma_
2010-03-17 07:36:54 +01:00
#
2010-03-22 08:33:44 +01:00
# Purpose: Keeps track of which nick has said what and when. Used in
2010-06-18 09:03:16 +02:00
# conjunction with OperatorStuff and Quotegrabs for kick/ban on flood
2010-03-22 08:33:44 +01:00
# and grabbing quotes, respectively.
2010-03-17 07:36:54 +01:00
package PBot::AntiFlood ;
use warnings ;
use strict ;
2010-06-18 06:17:30 +02:00
use feature 'switch' ;
2010-03-24 07:47:40 +01:00
use vars qw( $VERSION ) ;
$ VERSION = $ PBot:: PBot:: VERSION ;
2010-03-17 07:36:54 +01:00
use Time::HiRes qw( gettimeofday ) ;
2010-03-22 08:33:44 +01:00
use Carp ( ) ;
sub new {
if ( ref ( $ _ [ 1 ] ) eq 'HASH' ) {
Carp:: croak ( "Options to AntiFlood should be key/value pairs, not hash reference" ) ;
}
2010-03-17 07:36:54 +01:00
2010-03-22 08:33:44 +01:00
my ( $ class , % conf ) = @ _ ;
2010-03-17 07:36:54 +01:00
2010-03-22 08:33:44 +01:00
my $ self = bless { } , $ class ;
$ self - > initialize ( % conf ) ;
return $ self ;
}
2010-03-17 07:36:54 +01:00
2010-03-22 08:33:44 +01:00
sub initialize {
my ( $ self , % conf ) = @ _ ;
2010-03-17 07:36:54 +01:00
2010-03-22 08:33:44 +01:00
my $ pbot = delete $ conf { pbot } ;
if ( not defined $ pbot ) {
Carp:: croak ( "Missing pbot reference to AntiFlood" ) ;
}
$ self - > { pbot } = $ pbot ;
$ self - > { FLOOD_CHAT } = 0 ;
$ self - > { FLOOD_JOIN } = 1 ;
$ self - > { flood_msg_count } = 0 ;
$ self - > { last_timestamp } = gettimeofday ;
$ self - > { message_history } = { } ;
2010-03-23 19:24:02 +01:00
$ pbot - > timer - > register ( sub { $ self - > prune_message_history } , 60 * 60 * 1 ) ;
2010-06-16 03:55:29 +02:00
$ pbot - > commands - > register ( sub { return $ self - > unbanme ( @ _ ) } , "unbanme" , 0 ) ;
2010-03-22 08:33:44 +01:00
}
2010-03-17 07:36:54 +01:00
2010-06-06 07:19:31 +02:00
sub get_flood_account {
my ( $ self , $ nick , $ user , $ host ) = @ _ ;
return $ nick if exists $ { $ self - > message_history } { $ nick } ;
foreach my $ n ( keys % { $ self - > { message_history } } ) {
my $ userhost = "$user\@$host" ;
if ( $ { $ self - > { message_history } } { $ n } { hostmask } =~ /\Q$userhost\E/i ) {
$ self - > { pbot } - > logger - > log ( "Using existing hostmask found with nick $n\n" ) ;
return $ n ;
}
}
return undef ;
}
2010-03-17 07:36:54 +01:00
sub check_flood {
2010-06-05 08:07:15 +02:00
my ( $ self , $ channel , $ nick , $ user , $ host , $ text , $ max_messages , $ max_time , $ mode ) = @ _ ;
2010-03-17 07:36:54 +01:00
my $ now = gettimeofday ;
$ channel = lc $ channel ;
2010-03-26 06:14:03 +01:00
$ self - > { pbot } - > logger - > log ( sprintf ( "%-14s | %-65s | %s\n" , $ channel , "$nick!$user\@$host" , $ text ) ) ;
2010-06-05 08:07:15 +02:00
2010-03-22 08:33:44 +01:00
return if $ nick eq $ self - > { pbot } - > botnick ;
2010-03-17 07:36:54 +01:00
2010-06-06 07:19:31 +02:00
my $ account = $ self - > get_flood_account ( $ nick , $ user , $ host ) ;
2010-06-12 06:27:58 +02:00
if ( not defined $ account ) {
# new addition
#$self->{pbot}->logger->log("brand new nick addition\n");
$ { $ self - > message_history } { $ nick } { hostmask } = "$nick!$user\@$host" ;
2010-03-17 07:36:54 +01:00
2010-06-12 06:27:58 +02:00
$ account = $ nick ;
}
2010-06-05 08:07:15 +02:00
2010-06-12 06:27:58 +02:00
if ( not exists $ { $ self - > message_history } { $ account } { $ channel } ) {
#$self->{pbot}->logger->log("adding new channel for existing nick\n");
$ { $ self - > message_history } { $ account } { $ channel } { offenses } = 0 ;
$ { $ self - > message_history } { $ account } { $ channel } { join_watch } = 0 ;
$ { $ self - > message_history } { $ account } { $ channel } { messages } = [] ;
}
2010-03-17 07:36:54 +01:00
2010-06-12 06:27:58 +02:00
#$self->{pbot}->logger->log("appending new message\n");
push ( @ { $ { $ self - > message_history } { $ account } { $ channel } { messages } } , { timestamp = > $ now , msg = > $ text , mode = > $ mode } ) ;
2010-03-17 07:36:54 +01:00
2010-06-12 06:27:58 +02:00
my $ length = $# { $ { $ self - > message_history } { $ account } { $ channel } { messages } } + 1 ;
2010-03-17 07:36:54 +01:00
2010-06-12 06:27:58 +02:00
if ( $ length >= $ self - > { pbot } - > { MAX_NICK_MESSAGES } ) {
my % msg = % { shift ( @ { $ { $ self - > message_history } { $ account } { $ channel } { messages } } ) } ;
#$self->{pbot}->logger->log("shifting message off top: $msg{msg}, $msg{timestamp}\n");
$ length - - ;
}
2010-06-18 05:21:36 +02:00
return if ( $ channel =~ /^#/ ) and ( not exists $ self - > { pbot } - > channels - > channels - > hash - > { $ channel } or $ self - > { pbot } - > channels - > channels - > hash - > { $ channel } { chanop } == 0 ) ;
2010-06-12 06:27:58 +02:00
if ( $ mode == $ self - > { FLOOD_JOIN } ) {
if ( $ text =~ /^JOIN/ ) {
$ { $ self - > message_history } { $ account } { $ channel } { join_watch } + + ;
$ self - > { pbot } - > logger - > log ( "$nick $channel joinwatch adjusted: ${ $self->message_history }{$account}{$channel}{join_watch}\n" ) ;
} else {
2010-06-12 06:38:38 +02:00
# PART or QUIT
# if QUIT, then assume they existed on any channel the bot exists on
# this makes it possible to deal with ping timeout quits
2010-06-18 05:21:36 +02:00
foreach my $ chan ( keys % { $ self - > { pbot } - > channels - > channels - > hash } ) {
2010-06-12 06:38:38 +02:00
if ( not exists $ { $ self - > message_history } { $ account } { $ chan } ) {
$ { $ self - > message_history } { $ account } { $ chan } { offenses } = 0 ;
$ { $ self - > message_history } { $ account } { $ chan } { join_watch } = 0 ;
$ { $ self - > message_history } { $ account } { $ chan } { messages } = [] ;
}
2010-06-12 09:48:01 +02:00
push ( @ { $ { $ self - > message_history } { $ account } { $ chan } { messages } } , { timestamp = > $ now , msg = > $ text , mode = > $ mode } ) unless $ chan eq $ channel ;
2010-06-12 06:38:38 +02:00
}
# check QUIT message for netsplits, and decrement joinwatch if found
2010-06-12 06:27:58 +02:00
if ( $ text =~ /^QUIT .*\.net .*\.split/ ) {
foreach my $ ch ( keys % { $ self - > message_history - > { $ account } } ) {
next if $ ch eq 'hostmask' ; # TODO: move channels into {channel} subkey
2010-06-12 07:50:38 +02:00
next if $ ch !~ /^#/ ;
2010-06-12 06:27:58 +02:00
$ { $ self - > message_history } { $ account } { $ ch } { join_watch } - - ;
$ { $ self - > message_history } { $ account } { $ ch } { join_watch } = 0 if $ { $ self - > message_history } { $ account } { $ ch } { join_watch } < 0 ;
$ self - > { pbot } - > logger - > log ( "$nick $ch joinwatch adjusted: ${ $self->message_history }{$account}{$ch}{join_watch}\n" ) ;
}
2010-06-12 06:38:38 +02:00
}
# check QUIT message for Ping timeout
elsif ( $ text =~ /^QUIT Ping timeout/ ) {
2010-06-12 06:27:58 +02:00
# deal with ping timeouts agressively
2010-06-12 06:38:38 +02:00
foreach my $ ch ( keys % { $ self - > message_history - > { $ account } } ) {
next if $ ch eq 'hostmask' ; # TODO: move channels into {channel} subkey
2010-06-12 07:50:38 +02:00
next if $ ch !~ /^#/ ;
2010-06-12 06:38:38 +02:00
$ { $ self - > message_history } { $ account } { $ ch } { join_watch } + + ;
$ self - > { pbot } - > logger - > log ( "$nick $ch joinwatch adjusted: ${ $self->message_history }{$account}{$ch}{join_watch}\n" ) ;
}
2010-06-10 22:26:09 +02:00
}
}
2010-06-12 06:27:58 +02:00
} elsif ( $ mode == $ self - > { FLOOD_CHAT } ) {
2010-06-12 07:50:38 +02:00
# reset joinwatch if they send a message
2010-06-12 06:27:58 +02:00
$ { $ self - > message_history } { $ account } { $ channel } { join_watch } = 0 ;
}
2010-06-10 22:26:09 +02:00
2010-06-12 09:48:01 +02:00
if ( $ max_messages > $ self - > { pbot } - > { MAX_NICK_MESSAGES } ) {
$ self - > { pbot } - > logger - > log ( "Warning: max_messages greater than MAX_NICK_MESSAGES; truncating.\n" ) ;
$ max_messages = $ self - > { pbot } - > { MAX_NICK_MESSAGES } ;
}
2010-06-12 06:27:58 +02:00
if ( $ max_messages > 0 and $ length >= $ max_messages ) {
$ self - > { pbot } - > logger - > log ( "More than $max_messages messages, comparing time differences ($max_time)\n" ) if $ mode == $ self - > { FLOOD_JOIN } ;
2010-06-16 03:55:29 +02:00
2010-06-12 06:27:58 +02:00
my % msg = % { @ { $ { $ self - > message_history } { $ account } { $ channel } { messages } } [ $ length - $ max_messages ] } ;
my % last = % { @ { $ { $ self - > message_history } { $ account } { $ channel } { messages } } [ $ length - 1 ] } ;
2010-06-05 08:07:15 +02:00
2010-06-12 06:27:58 +02:00
$ self - > { pbot } - > logger - > log ( "Comparing " . int ( $ last { timestamp } ) . " against " . int ( $ msg { timestamp } ) . ": " . ( int ( $ last { timestamp } - $ msg { timestamp } ) ) . " seconds\n" ) if $ mode == $ self - > { FLOOD_JOIN } ;
2010-06-05 08:07:15 +02:00
2010-06-12 06:27:58 +02:00
if ( $ last { timestamp } - $ msg { timestamp } <= $ max_time && not $ self - > { pbot } - > admins - > loggedin ( $ channel , "$nick!$user\@$host" ) ) {
if ( $ mode == $ self - > { FLOOD_JOIN } ) {
if ( $ { $ self - > message_history } { $ account } { $ channel } { join_watch } >= $ max_messages ) {
$ { $ self - > message_history } { $ account } { $ channel } { offenses } + + ;
2010-06-16 03:55:29 +02:00
2010-06-18 02:49:50 +02:00
my $ timeout = ( 2 ** ( ( $ self - > message_history - > { $ account } { $ channel } { offenses } + 6 ) < 10 ? $ { $ self - > message_history } { $ account } { $ channel } { offenses } + 6 : 10 ) ) ;
2010-06-18 06:15:28 +02:00
my $ banmask = address_to_mask ( $ host ) ;
2010-06-18 09:03:16 +02:00
$ self - > { pbot } - > chanops - > ban_user_timed ( "*!$user\@$banmask\$##fix_your_connection" , $ channel , $ timeout * 60 * 60 ) ;
2010-06-16 03:55:29 +02:00
2010-06-18 06:15:28 +02:00
$ self - > { pbot } - > logger - > log ( "$nick!$user\@$banmask banned for $timeout hours due to join flooding (offense #${ $self->message_history }{$account}{$channel}{offenses}).\n" ) ;
2010-06-16 03:55:29 +02:00
2010-06-12 06:27:58 +02:00
$ timeout = "several" if ( $ timeout > 8 ) ;
2010-06-16 03:55:29 +02:00
my $ captcha = generate_random_string ( 7 ) ;
$ { $ self - > message_history } { $ account } { $ channel } { captcha } = $ captcha ;
$ self - > { pbot } - > conn - > privmsg ( $ nick , "You have been banned from $channel for $timeout hours due to join flooding. If your connection issues have been fixed, or this was an accident, you may request an unban by responding to this message with: unbanme $channel $captcha" ) ;
2010-06-12 06:27:58 +02:00
$ { $ self - > message_history } { $ account } { $ channel } { join_watch } = $ max_messages - 2 ; # give them a chance to rejoin
}
} elsif ( $ mode == $ self - > { FLOOD_CHAT } ) {
$ { $ self - > message_history } { $ account } { $ channel } { offenses } + + ;
my $ length = $ { $ self - > message_history } { $ account } { $ channel } { offenses } ** $ { $ self - > message_history } { $ account } { $ channel } { offenses } * $ { $ self - > message_history } { $ account } { $ channel } { offenses } * 30 ;
if ( $ channel =~ /^#/ ) { #channel flood (opposed to private message or otherwise)
2010-06-18 09:03:16 +02:00
return if exists $ self - > { pbot } - > chanops - > { unban_timeout } - > { "*!*\@$host" } ;
2010-06-12 06:27:58 +02:00
if ( $ mode == $ self - > { FLOOD_CHAT } ) {
2010-06-18 09:03:16 +02:00
$ self - > { pbot } - > chanops - > ban_user_timed ( "*!$user\@$host" , $ channel , $ length ) ;
2010-06-12 06:27:58 +02:00
2010-06-18 09:03:16 +02:00
$ self - > { pbot } - > logger - > log ( "$nick $channel flood offense ${ $self->message_history }{$account}{$channel}{offenses} earned $length second ban\n" ) ;
2010-06-05 08:07:15 +02:00
2010-05-30 04:02:29 +02:00
if ( $ length < 1000 ) {
$ length = "$length seconds" ;
} else {
$ length = "a little while" ;
}
2010-06-18 09:03:16 +02:00
$ self - > { pbot } - > conn - > privmsg ( $ nick , "You have been muted due to flooding. Please use a web paste service such as http://codepad.org for lengthy pastes. You will be allowed to speak again in $length." ) ;
2010-06-12 06:27:58 +02:00
}
} else { # private message flood
return if exists $ self - > { pbot } - > ignorelist - > { ignore_list } - > { "$nick!$user\@$host" } { $ channel } ;
$ self - > { pbot } - > logger - > log ( "$nick msg flood offense ${ $self->message_history }{$account}{$channel}{offenses} earned $length second ignore\n" ) ;
$ self - > { pbot } - > { ignorelistcmds } - > ignore_user ( "" , "floodcontrol" , "" , "" , "$nick!$user\@$host $channel $length" ) ;
if ( $ length < 1000 ) {
$ length = "$length seconds" ;
} else {
$ length = "a little while" ;
2010-05-30 04:02:29 +02:00
}
2010-06-12 06:27:58 +02:00
$ self - > { pbot } - > conn - > privmsg ( $ nick , "You have used too many commands in too short a time period, you have been ignored for $length." ) ;
2010-03-17 07:36:54 +01:00
}
}
}
}
}
2010-03-22 08:33:44 +01:00
sub message_history {
my $ self = shift ;
return $ self - > { message_history } ;
}
2010-03-23 19:24:02 +01:00
sub prune_message_history {
my $ self = shift ;
$ self - > { pbot } - > logger - > log ( "Pruning message history . . .\n" ) ;
2010-03-29 10:39:48 +02:00
foreach my $ nick ( keys % { $ self - > { message_history } } ) {
foreach my $ channel ( keys % { $ self - > { message_history } - > { $ nick } } )
2010-03-23 19:24:02 +01:00
{
2010-06-06 09:22:23 +02:00
next if $ channel eq 'hostmask' ; # TODO: move channels into {channel} subkey
2010-04-10 00:53:17 +02:00
#$self->{pbot}->logger->log("Checking [$nick][$channel]\n");
2010-03-29 10:39:48 +02:00
my $ length = $# { $ self - > { message_history } - > { $ nick } { $ channel } { messages } } + 1 ;
my % last = % { @ { $ self - > { message_history } - > { $ nick } { $ channel } { messages } } [ $ length - 1 ] } ;
2010-03-23 19:24:02 +01:00
2010-05-30 04:02:29 +02:00
if ( gettimeofday - $ last { timestamp } >= 60 * 60 * 24 * 3 ) {
$ self - > { pbot } - > logger - > log ( "$nick in $channel hasn't spoken in three days, removing message history.\n" ) ;
2010-03-29 10:39:48 +02:00
delete $ self - > { message_history } - > { $ nick } { $ channel } ;
2010-03-23 19:24:02 +01:00
}
}
}
}
2010-06-16 03:55:29 +02:00
sub unbanme {
my ( $ self , $ from , $ nick , $ user , $ host , $ arguments ) = @ _ ;
my ( $ channel , $ captcha ) = split / / , $ arguments ;
if ( not defined $ channel or not defined $ captcha ) {
return "/msg $nick Usage: unbanme <channel> <captcha>" ;
}
2010-06-18 06:15:28 +02:00
my $ banmask = address_to_mask ( $ host ) ;
my $ mask = "*!$user\@$banmask\$##fix_your_connection" ;
2010-06-16 03:55:29 +02:00
2010-06-18 09:03:16 +02:00
if ( not exists $ self - > { pbot } - > { chanops } - > { unban_timeout } - > { $ mask } ) {
2010-06-16 03:55:29 +02:00
return "/msg $nick There is no temporary ban set for $mask in channel $channel." ;
}
2010-06-18 09:03:16 +02:00
if ( not $ self - > { pbot } - > chanops - > { unban_timeout } - > { $ mask } { channel } eq $ channel ) {
2010-06-16 03:55:29 +02:00
return "/msg $nick There is no temporary ban set for $mask in channel $channel." ;
}
my $ account = $ self - > get_flood_account ( $ nick , $ user , $ host ) ;
if ( not defined $ account ) {
return "/msg $nick I do not remember you." ;
}
if ( not exists $ self - > { message_history } - > { $ account } { $ channel } { captcha } ) {
return "/msg $nick I do not remember banning you in $channel." ;
}
if ( not $ self - > { message_history } - > { $ account } { $ channel } { captcha } eq $ captcha ) {
return "/msg $nick Incorrect captcha." ;
}
# TODO: these delete statements need to be abstracted to methods on objects
2010-06-18 09:03:16 +02:00
$ self - > { pbot } - > chanops - > unban_user ( $ mask , $ channel ) ;
delete $ self - > { pbot } - > chanops - > { unban_timeout } - > { $ mask } ;
2010-06-16 03:55:29 +02:00
delete $ self - > { message_history } - > { $ account } { $ channel } { captcha } ;
return "/msg $nick You have been unbanned from $channel." ;
}
2010-06-18 06:15:28 +02:00
sub address_to_mask {
my $ address = shift ;
my $ banmask ;
if ( $ address =~ m/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/ ) {
my ( $ a , $ b , $ c , $ d ) = ( $ 1 , $ 2 , $ 3 , $ 4 ) ;
given ( $ a ) {
when ( $ _ <= 127 ) { $ banmask = "$a.*" ; }
when ( $ _ <= 191 ) { $ banmask = "$a.$b.*" ; }
default { $ banmask = "$a.$b.$c.*" ; }
}
} elsif ( $ address =~ m/[^.]+\.([^.]+\.[^.]+)$/ ) {
$ banmask = "*.$1" ;
} else {
$ banmask = $ address ;
}
return $ banmask ;
}
2010-06-16 03:55:29 +02:00
# based on Guy Malachi's code
sub generate_random_string {
my $ length_of_randomstring = shift ;
my @ chars = ( 'a' .. 'z' , 'A' .. 'Z' , '0' .. '9' , '_' ) ;
my $ random_string ;
foreach ( 1 .. $ length_of_randomstring ) {
$ random_string . = $ chars [ rand @ chars ] ;
}
return $ random_string ;
}
2010-03-17 07:36:54 +01:00
1 ;