diff --git a/bin/auto b/bin/auto index b872fc5..1fcae51 100755 --- a/bin/auto +++ b/bin/auto @@ -144,7 +144,7 @@ if (conf_get('die')) { } # Check for required configuration values. -my @REQCVALS = qw(locale expire_logs server fantasy_pf); +my @REQCVALS = qw(locale expire_logs server fantasy_pf ratelimit); foreach my $REQCVAL (@REQCVALS) { if (!conf_get($REQCVAL)) { my $err = 2; @@ -174,6 +174,9 @@ if (!-e "$Bin/../etc/auto.db") { } our $DB = DBI->connect("dbi:SQLite:dbname=$Bin/../etc/auto.db", q{}, q{}); +# Ratelimit timer. +Core::IRC::clear_usercmd_timer(); + # Parse privileges. our (%PRIVILEGES); # If there are any privsets. diff --git a/doc/CHANGELOG b/doc/CHANGELOG index 3877d68..c2fcbb8 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -3,6 +3,7 @@ Auto IRC Bot 3.0: Change Log 3.0 Indev =============================================================================== + * Added command rate limiting. * Added module installer to ./install. * Added a module buildsys. * Added a QDB module. diff --git a/lib/API/Std.pm b/lib/API/Std.pm index c7518d3..76780a8 100644 --- a/lib/API/Std.pm +++ b/lib/API/Std.pm @@ -11,7 +11,7 @@ use base qw(Exporter); our (%LANGE, %MODULE, %EVENTS, %HOOKS, %CMDS); our @EXPORT_OK = qw(conf_get trans err awarn timer_add timer_del cmd_add cmd_del hook_add hook_del rchook_add rchook_del match_user - has_priv mod_exists); + has_priv mod_exists ratelimit_check); # Initialize a module. @@ -415,6 +415,38 @@ sub has_priv return; } +# Ratelimit check subroutine. +sub ratelimit_check +{ + my (%src) = @_; + + # Check if ratelimit is set to on. + if ((conf_get('ratelimit'))[0][0] == 1) { + if (!defined $Core::IRC::usercmd{$src{nick}.'@'.$src{host}.'/'.$src{svr}}) { + # Set a usercmd entry for this user. + $Core::IRC::usercmd{$src{nick}.'@'.$src{host}.'/'.$src{svr}} = 0; + } + + # If the user has not passed the rate limit. + if ($Core::IRC::usercmd{$src{nick}.'@'.$src{host}.'/'.$src{svr}} <= (conf_get('ratelimit_amount'))[0][0]) { + # Increment their uses and return 1. + $Core::IRC::usercmd{$src{nick}.'@'.$src{host}.'/'.$src{svr}}++; + return 1; + } + else { + # Increment their uses and return 0. + $Core::IRC::usercmd{$src{nick}.'@'.$src{host}.'/'.$src{svr}}++; + return 0; + } + } + else { + # It isn't. Return 1. + return 1; + } + + return 1; +} + # Error subroutine. sub err ## no critic qw(Subroutines::ProhibitBuiltinHomonyms) { diff --git a/lib/Core/IRC.pm b/lib/Core/IRC.pm index 28a9863..8e1454b 100644 --- a/lib/Core/IRC.pm +++ b/lib/Core/IRC.pm @@ -1,13 +1,15 @@ -# lib/Core/IRC.pm - Core IRC hooks. +# lib/Core/IRC.pm - Core IRC hooks and timers. # Copyright (C) 2010-2011 Xelhua Development Group, et al. # This program is free software; rights to this code are stated in doc/LICENSE. package Core::IRC; use strict; use warnings; use English qw(-no_match_vars); -use API::Std qw(hook_add conf_get); +use API::Std qw(hook_add timer_add conf_get); use API::IRC qw(notice usrc); +our (%usercmd); + # CTCP VERSION reply. hook_add("on_uprivmsg", "ctcp_version_reply", sub { my (($svr, @ex)) = @_; @@ -84,6 +86,23 @@ hook_add("on_connect", "autojoin", sub { return 1; }); +sub clear_usercmd_timer +{ + # If ratelimit is set to 1 in config, add this timer. + if ((conf_get('ratelimit'))[0][0] eq 1) { + # Clear usercmd hash every X seconds. + timer_add('clear_usercmd', 2, (conf_get('ratelimit_time'))[0][0], sub { + foreach (keys %Core::IRC::usercmd) { + $Core::IRC::usercmd{$_} = 0; + } + + return 1; + }); + } + + return 1; +} + 1; # vim: set ai sw=4 ts=4: diff --git a/lib/Parser/IRC.pm b/lib/Parser/IRC.pm index 8f6b56d..3049b8c 100644 --- a/lib/Parser/IRC.pm +++ b/lib/Parser/IRC.pm @@ -4,7 +4,7 @@ package Parser::IRC; use strict; use warnings; -use API::Std qw(conf_get err awarn); +use API::Std qw(conf_get err awarn trans); use API::IRC; # Raw parsing hash. @@ -531,17 +531,31 @@ sub privmsg # It is coming to us in a private message. $cmd = uc(substr($ex[3], 1)); if (defined $API::Std::CMDS{$cmd}) { + # If this is indeed a command, continue. if ($API::Std::CMDS{$cmd}{lvl} == 1 or $API::Std::CMDS{$cmd}{lvl} == 2) { - if ($API::Std::CMDS{$cmd}{priv}) { - if (API::Std::has_priv(API::Std::match_user(%data), $API::Std::CMDS{$cmd}{priv})) { - &{ $API::Std::CMDS{$cmd}{sub} }(%data); + # Ensure the level is private or all. + if (!defined $Core::IRC::usercmd{$data{nick}.'@'.$data{host}}) { $Core::IRC::usercmd{$data{nick}.'@'.$data{host}} = 0; } + if (API::Std::ratelimit_check(%data)) { + # Continue if the user has not passed the ratelimit amount. + if ($API::Std::CMDS{$cmd}{priv}) { + # If this command requires a privilege... + if (API::Std::has_priv(API::Std::match_user(%data), $API::Std::CMDS{$cmd}{priv})) { + # Make sure they have it. + &{ $API::Std::CMDS{$cmd}{'sub'} }(%data); + } + else { + # Else give them the boot. + API::IRC::notice($data{svr}, $data{nick}, API::Std::trans("Permission denied")."."); + } } else { - API::IRC::notice($data{svr}, $data{nick}, API::Std::trans("Permission denied")."."); + # Else execute the command without any extra checks. + &{ $API::Std::CMDS{$cmd}{'sub'} }(%data); } } else { - &{ $API::Std::CMDS{$cmd}{sub} }(%data); + # Send them a notice about their bad deed. + API::IRC::notice($data{svr}, $data{nick}, trans('Rate limit exceeded').q{.}); } } } @@ -556,19 +570,33 @@ sub privmsg $rprefix = substr($ex[3], 1, 1); $cmd = uc(substr($ex[3], 2)); if (defined $API::Std::CMDS{$cmd}) { + # If this is indeed a command, continue. if ($API::Std::CMDS{$cmd}{lvl} == 0 or $API::Std::CMDS{$cmd}{lvl} == 2) { - if ($API::Std::CMDS{$cmd}{priv}) { - if (API::Std::has_priv(API::Std::match_user(%data), $API::Std::CMDS{$cmd}{priv})) { - &{ $API::Std::CMDS{$cmd}{sub} }(%data) if $rprefix eq $cprefix; + # Ensure the level is public or all. + if (!defined $Core::IRC::usercmd{$data{nick}.'@'.$data{host}}) { $Core::IRC::usercmd{$data{nick}.'@'.$data{host}} = 0; } + if (API::Std::ratelimit_check(%data)) { + # Continue if the user has not passed the ratelimit amount. + if ($API::Std::CMDS{$cmd}{priv}) { + # If this command takes a privilege... + if (API::Std::has_priv(API::Std::match_user(%data), $API::Std::CMDS{$cmd}{priv})) { + # Make sure they have it. + &{ $API::Std::CMDS{$cmd}{'sub'} }(%data) if $rprefix eq $cprefix; + } + else { + # Else give them the boot. + API::IRC::notice($data{svr}, $data{nick}, API::Std::trans("Permission denied")."."); + } } else { - API::IRC::notice($data{svr}, $data{nick}, API::Std::trans("Permission denied")."."); + # Else continue executing without any extra checks. + &{ $API::Std::CMDS{$cmd}{'sub'} }(%data) if $rprefix eq $cprefix; } } else { - &{ $API::Std::CMDS{$cmd}{sub} }(%data) if $rprefix eq $cprefix; + # Send them a notice about their bad deed. + API::IRC::notice($data{svr}, $data{nick}, trans('Rate limit exceeded').q{.}); } - } + } } # Trigger event on_cprivmsg.