#!/usr/bin/perl
#
use strict;
no warnings 'uninitialized';
package LJ;
#
# name: LJ::sysban_check
# des: Given a 'what' and 'value', checks to see if a ban exists.
# args: what, value
# des-what: The ban type
# des-value: The value which triggers the ban
# returns: 1 if a ban exists, 0 otherwise
#
sub sysban_check {
my ($what, $value) = @_;
# cache if ip ban
if ($what eq 'ip') {
my $now = time();
my $ip_ban_delay = $LJ::SYSBAN_IP_REFRESH || 120;
# check memcache first if not loaded
unless ($LJ::IP_BANNED_LOADED + $ip_ban_delay > $now) {
my $memval = LJ::MemCache::get("sysban:ip");
if ($memval) {
*LJ::IP_BANNED = $memval;
$LJ::IP_BANNED_LOADED = $now;
} else {
$LJ::IP_BANNED_LOADED = 0;
}
}
# is it already cached in memory?
if ($LJ::IP_BANNED_LOADED) {
return (defined $LJ::IP_BANNED{$value} &&
($LJ::IP_BANNED{$value} == 0 || # forever
$LJ::IP_BANNED{$value} > time())); # not-expired
}
# set this before the query
$LJ::IP_BANNED_LOADED = time();
LJ::sysban_populate(\%LJ::IP_BANNED, "ip")
or return undef $LJ::IP_BANNED_LOADED;
# set in memcache
LJ::MemCache::set("sysban:ip", \%LJ::IP_BANNED, $ip_ban_delay);
# return value to user
return $LJ::IP_BANNED{$value};
}
# cache if uniq ban
if ($what eq 'uniq') {
# check memcache first if not loaded
unless ($LJ::UNIQ_BANNED_LOADED) {
my $memval = LJ::MemCache::get("sysban:uniq");
if ($memval) {
*LJ::UNIQ_BANNED = $memval;
$LJ::UNIQ_BANNED_LOADED++;
}
}
# is it already cached in memory?
if ($LJ::UNIQ_BANNED_LOADED) {
return (defined $LJ::UNIQ_BANNED{$value} &&
($LJ::UNIQ_BANNED{$value} == 0 || # forever
$LJ::UNIQ_BANNED{$value} > time())); # not-expired
}
# set this now before the query
$LJ::UNIQ_BANNED_LOADED++;
LJ::sysban_populate(\%LJ::UNIQ_BANNED, "uniq")
or return undef $LJ::UNIQ_BANNED_LOADED;
# set in memcache
my $exp = 60*15; # 15 minutes
LJ::MemCache::set("sysban:uniq", \%LJ::UNIQ_BANNED, $exp);
# return value to user
return $LJ::UNIQ_BANNED{$value};
}
# cache if contentflag ban
if ($what eq 'contentflag') {
# check memcache first if not loaded
unless ($LJ::CONTENTFLAG_BANNED_LOADED) {
my $memval = LJ::MemCache::get("sysban:contentflag");
if ($memval) {
*LJ::CONTENTFLAG_BANNED = $memval;
$LJ::CONTENTFLAG_BANNED_LOADED++;
}
}
# is it already cached in memory?
if ($LJ::CONTENTFLAG_BANNED_LOADED) {
return (defined $LJ::CONTENTFLAG_BANNED{$value} &&
($LJ::CONTENTFLAG_BANNED{$value} == 0 || # forever
$LJ::CONTENTFLAG_BANNED{$value} > time())); # not-expired
}
# set this now before the query
$LJ::CONTENTFLAG_BANNED_LOADED++;
LJ::sysban_populate(\%LJ::CONTENTFLAG_BANNED, "contentflag")
or return undef $LJ::CONTENTFLAG_BANNED_LOADED;
# set in memcache
my $exp = 60*15; # 15 minutes
LJ::MemCache::set("sysban:contentflag", \%LJ::CONTENTFLAG_BANNED, $exp);
# return value to user
return (defined $LJ::CONTENTFLAG_BANNED{$value} &&
($LJ::CONTENTFLAG_BANNED{$value} == 0 || # forever
$LJ::CONTENTFLAG_BANNED{$value} > time())); # not-expired
}
# need the db below here
my $dbr = LJ::get_db_reader();
return undef unless $dbr;
# standard check helper
my $check = sub {
my ($wh, $vl) = @_;
return $dbr->selectrow_array(qq{
SELECT COUNT(*)
FROM sysban
WHERE status = 'active'
AND what = ?
AND value = ?
AND NOW() > bandate
AND (NOW() < banuntil
OR banuntil = 0
OR banuntil IS NULL)
}, undef, $wh, $vl);
};
# check both ban by email and ban by domain if we have an email address
if ($what eq 'email') {
# short out if this email really is banned directly, or if we can't parse it
return 1 if $check->('email', $value);
return 0 unless $value =~ /@(.+)$/;
# see if this domain is banned
my @domains = split(/\./, $1);
return 0 unless scalar @domains >= 2;
return 1 if $check->('email_domain', "$domains[-2].$domains[-1]");
# must not be banned
return 0;
}
# non-ip bans come straight from the db
return $check->($what, $value);
}
# takes a hashref to populate with 'value' => expiration pairs
# takes a 'what' to populate the hashref with sysbans of that type
# returns undef on failure, hashref on success
sub sysban_populate {
my ($where, $what) = @_;
# call normally if no gearman/not wanted
my $gc = LJ::gearman_client();
return LJ::_db_sysban_populate($where, $what)
unless $gc && LJ::conf_test($LJ::LOADSYSBAN_USING_GEARMAN);
# invoke gearman
my $args = Storable::nfreeze({what => $what});
my $task = Gearman::Task->new("sysban_populate", \$args,
{
uniq => $what,
on_complete => sub {
my $res = shift;
return unless $res;
my $rv = Storable::thaw($$res);
return unless $rv;
$where->{$_} = $rv->{$_} foreach keys %$rv;
}
});
my $ts = $gc->new_task_set();
$ts->add_task($task);
$ts->wait(timeout => 30); # 30 sec timeout
return $where;
}
sub _db_sysban_populate {
my ($where, $what) = @_;
my $dbh = LJ::get_db_writer();
return undef unless $dbh;
# build cache from db
my $sth = $dbh->prepare("SELECT value, UNIX_TIMESTAMP(banuntil) FROM sysban " .
"WHERE status='active' AND what=? " .
"AND NOW() > bandate " .
"AND (NOW() < banuntil OR banuntil IS NULL)");
$sth->execute($what);
return undef if $sth->err;
while (my ($val, $exp) = $sth->fetchrow_array) {
$where->{$val} = $exp || 0;
}
return $where;
}
#
# name: LJ::sysban_note
# des: Inserts a properly-formatted row into [dbtable[statushistory]] noting that a ban has been triggered.
# args: userid?, notes, vars
# des-userid: The userid which triggered the ban, if available.
# des-notes: A very brief description of what triggered the ban.
# des-vars: A hashref of helpful variables to log, keys being variable name and values being values.
# returns: nothing
#
sub sysban_note
{
my ($userid, $notes, $vars) = @_;
$notes .= ":";
map { $notes .= " $_=$vars->{$_};" if $vars->{$_} } sort keys %$vars;
LJ::statushistory_add($userid, 0, 'sysban_trig', $notes);
return;
}
#
# name: LJ::sysban_block
# des: Notes a sysban in [dbtable[statushistory]] and returns a fake HTTP error message to the user.
# args: userid?, notes, vars
# des-userid: The userid which triggered the ban, if available.
# des-notes: A very brief description of what triggered the ban.
# des-vars: A hashref of helpful variables to log, keys being variable name and values being values.
# returns: nothing
#
sub sysban_block
{
my ($userid, $notes, $vars) = @_;
LJ::sysban_note($userid, $notes, $vars);
my $msg = <<'EOM';
503 Service Unavailable
503 Service Unavailable
The service you have requested is temporarily unavailable.
EOM
# may not run from web context (e.g. mailgated.pl -> supportlib -> ..)
eval { BML::http_response(200, $msg); };
return;
}
#
# name: LJ::sysban_create
# des: creates a sysban.
# args: what, value, bandays, note
# des-what: the criteria we're sysbanning on
# des-value: the value we're banning
# des-bandays: length of sysban (0 for forever)
# des-note: note to go with the ban (optional)
# info: Takes args as a hash.
# returns: 1 on success, 0 on failure
#
sub sysban_create {
my %opts = @_;
my $dbh = LJ::get_db_writer();
my $banuntil = "NULL";
if ($opts{'bandays'}) {
$banuntil = "NOW() + INTERVAL " . $dbh->quote($opts{'bandays'}) . " DAY";
}
# strip out leading/trailing whitespace
$opts{'value'} = LJ::trim($opts{'value'});
# do insert
$dbh->do("INSERT INTO sysban (what, value, note, bandate, banuntil) VALUES (?, ?, ?, NOW(), $banuntil)",
undef, $opts{'what'}, $opts{'value'}, $opts{'note'});
return $dbh->errstr if $dbh->err;
my $banid = $dbh->{'mysql_insertid'};
my $exptime = $opts{bandays} ? time() + 86400*$opts{bandays} : 0;
# special case: creating ip/uniq ban
if ($opts{'what'} eq 'ip') {
LJ::procnotify_add("ban_ip", { 'ip' => $opts{'value'}, exptime => $exptime });
LJ::MemCache::delete("sysban:ip");
}
if ($opts{'what'} eq 'uniq') {
LJ::procnotify_add("ban_uniq", { 'uniq' => $opts{'value'}, exptime => $exptime});
LJ::MemCache::delete("sysban:uniq");
}
if ($opts{'what'} eq 'contentflag') {
LJ::procnotify_add("ban_contentflag", { 'username' => $opts{'value'}, exptime => $exptime});
LJ::MemCache::delete("sysban:contentflag");
}
# log in statushistory
my $remote = LJ::get_remote();
$banuntil = $opts{'bandays'} ? LJ::mysql_time($exptime) : "forever";
LJ::statushistory_add(0, $remote, 'sysban_add',
"banid=$banid; status=active; " .
"bandate=" . LJ::mysql_time() . "; banuntil=$banuntil; " .
"what=$opts{'what'}; value=$opts{'value'}; " .
"note=$opts{'note'};");
return $banid;
}
#
# name: LJ::sysban_validate
# des: determines whether a sysban can be added for a given value.
# args: type, value
# des-type: the sysban type we're checking
# des-value: the value we're checking
# returns: nothing on success, error message on failure
#
sub sysban_validate {
my ($what, $value, $opts) = @_;
# bail early if the ban already exists
return "This is already banned"
if !$opts->{skipexisting} && LJ::sysban_check($what, $value);
my $validate = {
'ip' => sub {
my $ip = shift;
while (my ($ip_re, $reason) = each %LJ::UNBANNABLE_IPS) {
next unless $ip =~ $ip_re;
return "Cannot ban IP $ip: " . LJ::ehtml($reason);
}
return $ip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ ?
0 : "Format: xxx.xxx.xxx.xxx (ip address)";
},
'uniq' => sub {
my $uniq = shift;
return $uniq =~ /^[a-zA-Z0-9]{15}$/ ?
0 : "Invalid uniq: must be 15 digits/chars";
},
'email' => sub {
my $email = shift;
my @err;
LJ::check_email($email, \@err);
return @err ? shift @err : 0;
},
'email_domain' => sub {
my $email_domain = shift;
if ($email_domain =~ /^[^@]+\.[^@]+$/) {
return 0;
} else {
return "Invalid email domain: $email_domain";
}
},
'user' => sub {
my $user = shift;
my $u = LJ::load_user($user);
return $u ? 0 : "Invalid user: $user";
},
'pay_cc' => sub {
my $cc = shift;
return $cc =~ /^\d{4}-\d{4}$/ ?
0 : "Format: xxxx-xxxx (first four-last four)";
},
'msisdn' => sub {
my $num = shift;
return $num =~ /\d{10}/ ? 0 : 'Format: 10 digit MSISDN';
},
};
# aliases to handlers above
my @map = ('pay_user' => 'user',
'pay_email' => 'email',
'pay_uniq' => 'uniq',
'support_user' => 'user',
'support_email' => 'email',
'support_uniq' => 'uniq',
'lostpassword' => 'user',
'talk_ip_test' => 'ip',
'contentflag' => 'user',
);
while (my ($new, $existing) = splice(@map, 0, 2)) {
$validate->{$new} = $validate->{$existing};
}
my $check = $validate->{$what} or return "Invalid sysban type";
return $check->($value);
}
1;