Package: interchange Version: 5.5.0 Sessions per IP limit protection needs more flexibility. Currently the time it takes for a lockout to expire is the same amount of time that a particular IP has to go without any new sessions in order for the counter to reset and defaults to 24 hours. The fact that you have to wait 24 hours without any new sessions means that it is possible that someone who visits the site only once per day, but does so consistently will eventually get locked out no matter how high RobotLimit is set to.
Following are my notes on the attached patch which is well tested by this point (I have been running a production install with this patch for several months now)... This patch makes the following changes to sessions per IP limit protection: * There are now seperate settings for the amount of time allowed to trigger a lockout and the amount of time a lockout lasts for. * Lockouts are now triggered based on new sessions per time limit, which is different from the old critera of new sessions between pauses of length time. This means that if RobotLimit is set to 100 and the other settings were left at thier defaults then a 24 hour lockout would be triggered if a given IP address had 100 new sessions in any given 60 minute time period. Settings used by this patch: RobotLimit: Used to determine the number of new sessions required to trigger a lockout. Default is 0 which disables this feature alltogether. Limit robot_expire: Used to determine the amount of time a lockout will last in days once triggered. Can be less than 1, for example 0.04 is slightly less than an hour. Default is 1. Limit ip_session_expire: Used to determine the length of time in minutes for RobotLimit sessions to build up in the counter file and trigger a lockout. Default is 60 (1 hour). This can also be set to fractional numbers, for example 0.5 will allow 30 seconds. Also make note of the following: * When first implementing you should delete all the old counters with: rm -rf catroot/tmp/addr_ctr/* ...be careful with the above command, if mistyped it can seriously mess up your filesystem. * Shell command to view the contents of one of the binary new counter files: perl -pe 's/(.{4})/localtime(unpack("N",$1))."\n"/seg;' < 0_0_0_0 ...where 0_0_0_0 is the filename of the binary counter. The command will show you a list of timestamps in human readable form.
Index: lib/Vend/Dispatch.pm =================================================================== RCS file: /var/cvs/interchange/lib/Vend/Dispatch.pm,v retrieving revision 1.74 diff -u -p -r1.74 Dispatch.pm --- lib/Vend/Dispatch.pm 22 Sep 2006 07:10:03 -0000 1.74 +++ lib/Vend/Dispatch.pm 26 Jan 2007 05:24:04 -0000 @@ -1368,34 +1368,32 @@ RESOLVEID: { } } } - } - else { - if($Vend::Cfg->{RobotLimit}) { - if (Vend::Session::count_ip() > $Vend::Cfg->{RobotLimit}) { - my $msg; - # Here they can get it back if they pass expiration time - my $wait = $::Limit->{robot_expire} || 1; - $wait *= 24; - $msg = errmsg(<<EOF, $wait); + } else { + if (Vend::Session::count_ip()) { + my $msg; + # Here they can get it back if they pass expiration time + my $wait = $::Limit->{robot_expire} || 1; + $wait *= 24; + $msg = errmsg(<<EOF, $wait); Too many new ID assignments for this IP address. Please wait at least %d hours before trying again. Only waiting that period will allow access. Terminating. EOF - $msg = get_locale_message(403, $msg); - do_lockout(); + $msg = get_locale_message(403, $msg); + do_lockout(); - ::logError('Too many IDs, %d hour wait enforced.', $wait); + ::logError('Too many IDs, %d hour wait enforced.', $wait); - $Vend::StatusLine = <<EOF; + $Vend::StatusLine = <<EOF; Status: 403 Forbidden Content-Type: text/plain EOF - response($msg); - close_cat(); - return; - } - } - new_session(); - } + response($msg); + close_cat(); + return; + } + new_session(); + } + } #::logDebug("session name='$Vend::SessionName'\n"); Index: lib/Vend/Session.pm =================================================================== RCS file: /var/cvs/interchange/lib/Vend/Session.pm,v retrieving revision 2.27 diff -u -p -r2.27 Session.pm --- lib/Vend/Session.pm 20 Sep 2006 18:52:06 -0000 2.27 +++ lib/Vend/Session.pm 26 Jan 2007 05:24:04 -0000 @@ -195,22 +195,68 @@ sub open_session { } sub count_ip { - my $inc = shift; - my $ip = $CGI::remote_addr; - $ip =~ s/\W/_/g; - my $dir = "$Vend::Cfg->{ScratchDir}/addr_ctr"; - my $fn = Vend::Util::get_filename($ip, 2, 1, $dir); - if(-f $fn) { - my $grace = $::Limit->{robot_expire} || 1; - my @st = stat(_); - my $mtime = (time() - $st[9]) / 86400; - if($mtime > $grace) { - ::logDebug("ip $ip allowed back in due to '$mtime' > '$grace' days"); - unlink $fn; - } - } - return Vend::CounterFile->new($fn)->inc() if $inc; - return Vend::CounterFile->new($fn)->value(); + my ($inc) = @_; + + # Immediate return if RobotLimit is not defined + my $index = $Vend::Cfg->{RobotLimit} or return 0; + $index *= -1; + + + my $ip = $CGI::remote_addr; + $ip =~ s/\W/_/g; + + my $dir = "$Vend::Cfg->{ScratchDir}/addr_ctr"; + my $fn = Vend::Util::get_filename($ip, 2, 1, $dir); + + + # Unlink the "counter" file if applicable. + + if(-f $fn) { + my $grace = $::Limit->{ip_session_expire} || 60; + my @st = stat(_); + my $mtime = (time() - $st[9]) / 60; + if($mtime > $grace) { +#::logDebug("ip $ip session limit expired due to '$mtime' > '$grace' minutes"); + unlink $fn; + } + } + + my $lfn = $fn . '.lockout'; + + + # Unlink the lockout file if applicable, otherwise lock. + + if(-f $lfn) { + my $grace = $::Limit->{robot_expire} || 1; + my @st = stat(_); + my $mtime = (time() - $st[9]) / 86400; + if($mtime > $grace) { +#::logDebug("ip $ip allowed back in due to '$mtime' > '$grace' days"); + unlink $lfn; + } else { + return 1; + } + } + + + # Append a new timestamp to the counter file (if applicable) + + timecard_stamp($fn) if $inc; + + + # Get timestamp from timecard file and see if it's expired yet. + + my $rtime; + return 0 unless $rtime = timecard_read($fn, $index); + my $grace = $::Limit->{ip_session_expire} || 60; + return 0 if time - $rtime > $grace * 60; + +#::logDebug("ip $ip locked out due to too many new sessions in the last $grace minutes"); + # Create the lockout file + open(FH, '>', $lfn) or die "Can't create $lfn: $!"; + close FH; + + return 1; } sub is_retired { Index: lib/Vend/Util.pm =================================================================== RCS file: /var/cvs/interchange/lib/Vend/Util.pm,v retrieving revision 2.101 diff -u -p -r2.101 Util.pm --- lib/Vend/Util.pm 21 Nov 2006 22:32:12 -0000 2.101 +++ lib/Vend/Util.pm 26 Jan 2007 05:24:05 -0000 @@ -69,6 +69,8 @@ require Exporter; show_times string_to_ref tag_nitems + timecard_stamp + timecard_read uneval uneval_it uneval_fast @@ -2163,6 +2165,48 @@ sub codedef_options { } return [EMAIL PROTECTED]; } + + +# Adds a timestamp to the end of a binary timecard file. You can specify the timestamp +# as the second arg (unixtime) or just leave it out (or undefined) and it will be set +# to the current time. +sub timecard_stamp { + my ($filename,$timestamp) = @_; + $timestamp ||= time; + + open(FH, '>>', $filename) or die "Can't open $filename for append: $!"; + lockfile(\*FH, 1, 1); + print FH pack('N',time); + unlockfile(\*FH); + close FH; +} + + +# Reads a timestamp from a binary timecard file. If $index is negative indexes back from +# the end of the file, otherwise indexes from the front of the file so that 0 is the first +# (oldest) timestamp and -1 the last (most recent). Returns the timestamp or undefined if +# the file doesn't exist or the index falls outside of the bounds of the timecard file. +sub timecard_read { + my ($filename,$index) = @_; + $index *= 4; + my $limit = $index >= 0 ? $index + 4 : $index * -1; + + return undef unless (-f $filename && (stat(_))[7] > $limit); + + # The file exists and is big enough to cover the $index. Seek to the $index + # and return the timestamp from that position. + + open (FH, '<', $filename) or die "Can't open $filename for read: $!"; + lockfile(\*FH, 0, 1); + seek(FH, $index, $index >= 0 ? 0 : 2) or die "Can't seek $filename to $index: $!"; + my $rtime; + read(FH,$rtime,4) or die "Can't read from $filename: $!"; + unlockfile(\*FH); + close FH; + + return unpack('N',$rtime); +} + ### Provide stubs for former Vend::Util functions relocated to Vend::File *canonpath = \&Vend::File::canonpath;