# $Id: LoadLimit.pm,v 1.3 2009/01/15 00:50:53 hlein Exp $ # # Copyright KoreLogic 2009 # # A common routine for load-limiting CGIs # # Load-limit ourselves, so CGIs aren't such DoS potential; if # the checks fail then print a "sorry the server is too # busy" type error and bail (be sure to fake IIS 4.0 headers # in that case ;) # # Have CGIs run CheckLoad before doing any real work. # By default, all the CGI has to do is call 'CheckLoad,' # and a default 'sorry please try again' error will be # returned and the script will exit if the load is too high. # # There are a couple of customizable knobs: # -the load limits can be tweaked by the calling CGI # -the error message returned can be tweaked # -the calling CGI can choose *not* to have the error # returned to the user automatically, but can try to # "do the right thing" itself (an error will still be # logged) package LoadLimit; use Exporter; @ISA = ('Exporter'); # This is automatically available to the caller @EXPORT = qw(CheckLoad); # These are available only if "asked for" at load time @EXPORT_OK = qw(SetLimit PrintError SetError); # Load-checking watermarks: my $MAX_LOAD = 5; # first number of a traditional "load average" my $MAX_RUNNING = 15; # number of runnable processes my $MAX_TOTAL = 600; # total number of processes on the system # This set of attributes are used to build the error-response page. # They try to be sensible, generic defaults. Any/all of them can be # overriden by passing an appropriate key/value list to SetError. my (%Error); $Error{'Title'} = 'Problem performing your query'; $Error{'BodyScheme'} = '
'; $Error{'ErrTop'} = "Click here " .
"to return to the top level of the website. If the problem " .
"persists, contact the webmaster.\n";
# If the query was a GET, we can easily provide a link back to ourselves.
# If it's a POST, that's much harder, and not worth trying.
if ($ENV{REQUEST_METHOD} eq 'GET')
{
$Error{'ErrLink'} =
"Please click " .
"here to try your query again, or try again in a few minutes.\n";
}
elsif ($ENV{REQUEST_METHOD} eq 'POST')
{
$Error{'ErrLink'} =
"Please click here to return to " .
"the previous page and try again in a few minutes.\n";
}
# This'll set MAX_TOTAL arbitrarily low (to deliberately trigger the
# sorry-please-try-again code) if the calling script name contains 'test'
#if ($0 =~ m/test/) { $MAX_TOTAL = 60; };
# This is the optional interface to set the load limits to something
# different. Call passing key/value pairs with any/all of
# 'LOAD', 'RUNNING', and 'TOTAL' as keys, and numeric values, such
# as SetLimit('LOAD', 4, 'RUNNING', 20);
sub SetLimit
{
my %Limits = @_;
$MAX_LOAD = $Limits{LOAD} if ($Limits{LOAD});
$MAX_RUNNING = $Limits{RUNNING} if ($Limits{RUNNING});
$MAX_TOTAL = $Limits{TOTAL} if ($Limits{TOTAL});
}
# This function gets the load best it can and:
# -calls PrintError if the load is too high, or
# -returns a load avg associative array with keys LOAD, RUNNING,
# and TOTAL if called with the parameter "checkonly"
# -if load isn't too high, return the above assoc array (hey, why not?)
sub CheckLoad
{
# If not a POST or a GET with an actual query, we've already done 90%
# of the work we're gonna just by starting up; we may as well continue
return 1 unless (($ENV{QUERY_STRING} and $ENV{REQUEST_METHOD} eq 'GET') or
($ENV{REQUEST_METHOD} eq 'POST'));
# allow cgis to load LoadLimit and call CheckLoad directly,
# preempting a default call to CheckLoad in cgi-lib.pl
$LOADLIMIT_INSTALLED ? return : $LOADLIMIT_INSTALLED++;
# allow cgis to call CheckLoad telling it to only return the
# load info to the caller, instead of automagically printing
# an error and exiting if the load is too high
my $checkonly = (scalar(@_) && $_[0] =~ /checkonly/i) ? 1 : 0;
my(@today) = localtime;
$today[4]++; $today[5]+=1900;
$date = sprintf("%4d-%02d-%02d_%02d:%02d:%02d",$today[5],$today[4],
$today[3],$today[2],$today[1],$today[0]);
my ($load,$curload,$running,$total,$uptime);
if ( (open(LOAD, " and
# the line looks like: 0.03 0.08 0.03 2/81 11365
# we'll snag these: ^ ^ ^
$load =~ m%^([0-9]+)\.[0-9]+ [0-9.]+ [0-9.]+ ([0-9]+)/([0-9]+) [0-9]+$%
) or (
local($ENV{PATH})=undef and # prevent a warning under -T
$load = `/usr/bin/uptime` and # more portable than /proc
# the line looks like:
# 4:43pm up 5 days, 21:20, 1 user, load average: 0.05, 0.08, 0.03
# we'll snag these: ^ ^
$load =~ m%([0-9]+) users?, *load average: ([0-9]+)\.%
)
)
{
close(LOAD);
if (defined($3))
{
$curload = $1; $running = $2; $total = $3;
}
else
{
$curload = $2; $running = $2; $total = $1;
}
if ($curload > $MAX_LOAD or
$running > $MAX_RUNNING or
$total > $MAX_TOTAL)
{
# We're probably being DOSed; log an error and either kill
# ourselves, or return the load numbers to the caller
chomp $load;
print STDERR "[$date] ${0}[$$]: Abnormal load: $load for ",
"$ENV{'REMOTE_HOST'}:$ENV{'REQUEST_URI'}\n";
$checkonly ? return('LOAD',$curload,'RUNNING',$running,'TOTAL',$total)
: &PrintError;
}
}
else
{
print STDERR "[$date] ${0}[$$]: Error checking load: $! for ",
"$ENV{'REMOTE_HOST'}:$ENV{'REQUEST_URI'}\n";
close(LOAD);
$checkonly ? return(undef) : &PrintError;
}
return('LOAD',$curload,'RUNNING',$running,'TOTAL',$total);
}
# This is the optional interface to set the error strings to something
# different. Call passing key/value pairs with any/all of
# Title, BodyScheme, ErrTop, ErrLink, ErrBot, and strings, such as:
# SetError('Title', 'Problem trying to send email'), etc.
sub SetError
{
my %err = @_;
foreach my $key (%err)
{
$Error{$key} = $err{$key} if ($Error{$key});
}
}
# The error-printing function. Note that this assumes that the HTTP
# header has not yet been printed. If a script wants to issue custom
# messages to the user that don't fit into the %Error message array,
# it can't use this.
sub PrintError
{
print <