5 no warnings
'uninitialized';
10 use LJ
::CaptchaServer
;
13 # name: LJ::sysban_check
14 # des: Given a 'what' and 'value', checks to see if a ban exists.
16 # des-what: The ban type
17 # des-value: The value which triggers the ban
18 # returns: 1 if a ban exists, 0 otherwise
21 my ($what, $value, $opts) = @_;
26 my $is_force_use = defined $opts->{'force_use'} ?
$opts->{'force_use'} : 0;
29 if !$is_force_use && $LJ::DISABLED
{'sysban'};
31 warn "SYSBAN: Loading sysban ip list";
34 my $ip_ban_delay = $LJ::SYSBAN_IP_REFRESH
|| 120;
36 # check memcache first if not loaded
37 unless ($LJ::IP_BANNED_LOADED
+ $ip_ban_delay > $now) {
38 my $memval = LJ
::MemCache
::get
("sysban:ip");
40 *LJ
::IP_BANNED
= $memval;
41 $LJ::IP_BANNED_LOADED
= $now;
43 $LJ::IP_BANNED_LOADED
= 0;
47 # is it already cached in memory?
48 if ($LJ::IP_BANNED_LOADED
) {
49 return (defined $LJ::IP_BANNED
{$value} &&
50 ($LJ::IP_BANNED
{$value} == 0 || # forever
51 $LJ::IP_BANNED
{$value} > time())); # not-expired
54 # set this before the query
55 $LJ::IP_BANNED_LOADED
= time();
57 LJ
::sysban_populate
(\
%LJ::IP_BANNED
, "ip")
58 or return undef $LJ::IP_BANNED_LOADED
;
61 LJ
::MemCache
::set
("sysban:ip", \
%LJ::IP_BANNED
, $ip_ban_delay);
63 # return value to user
64 return $LJ::IP_BANNED
{$value};
67 elsif ($what eq 'ip_captcha'){
69 my $ip_ban_delay = $LJ::SYSBAN_IP_REFRESH
|| 120;
71 # check memcache first if not loaded
72 unless ($LJ::IP_CAPTCHA_BANNED_LOADED
+ $ip_ban_delay > $now) {
73 my $memval = LJ
::MemCache
::get
("sysban:ip_captcha");
75 *LJ
::IP_CAPTCHA_BANNED
= $memval;
76 $LJ::IP_CAPTCHA_BANNED_LOADED
= $now;
78 $LJ::IP_CAPTCAHA_BANNED_LOADED
= 0;
80 return exists $LJ::IP_CAPTCHA_BANNED
{$value}
81 ?
$LJ::IP_CAPTCHA_BANNED
{$value}
86 elsif ($what eq 'uniq') {
88 # check memcache first if not loaded
89 unless ($LJ::UNIQ_BANNED_LOADED
) {
90 my $memval = LJ
::MemCache
::get
("sysban:uniq");
92 *LJ
::UNIQ_BANNED
= $memval;
93 $LJ::UNIQ_BANNED_LOADED
++;
97 # is it already cached in memory?
98 if ($LJ::UNIQ_BANNED_LOADED
) {
99 return (defined $LJ::UNIQ_BANNED
{$value} &&
100 ($LJ::UNIQ_BANNED
{$value} == 0 || # forever
101 $LJ::UNIQ_BANNED
{$value} > time())); # not-expired
104 # set this now before the query
105 $LJ::UNIQ_BANNED_LOADED
++;
107 LJ
::sysban_populate
(\
%LJ::UNIQ_BANNED
, "uniq")
108 or return undef $LJ::UNIQ_BANNED_LOADED
;
111 my $exp = 60*15; # 15 minutes
112 LJ
::MemCache
::set
("sysban:uniq", \
%LJ::UNIQ_BANNED
, $exp);
114 # return value to user
115 return $LJ::UNIQ_BANNED
{$value};
117 # cache if contentflag ban
118 elsif ($what eq 'contentflag') {
120 # check memcache first if not loaded
121 unless ($LJ::CONTENTFLAG_BANNED_LOADED
) {
122 my $memval = LJ
::MemCache
::get
("sysban:contentflag");
124 *LJ
::CONTENTFLAG_BANNED
= $memval;
125 $LJ::CONTENTFLAG_BANNED_LOADED
++;
129 # is it already cached in memory?
130 if ($LJ::CONTENTFLAG_BANNED_LOADED
) {
131 return (defined $LJ::CONTENTFLAG_BANNED
{$value} &&
132 ($LJ::CONTENTFLAG_BANNED
{$value} == 0 || # forever
133 $LJ::CONTENTFLAG_BANNED
{$value} > time())); # not-expired
136 # set this now before the query
137 $LJ::CONTENTFLAG_BANNED_LOADED
++;
139 LJ
::sysban_populate
(\
%LJ::CONTENTFLAG_BANNED
, "contentflag")
140 or return undef $LJ::CONTENTFLAG_BANNED_LOADED
;
143 my $exp = 60*15; # 15 minutes
144 LJ
::MemCache
::set
("sysban:contentflag", \
%LJ::CONTENTFLAG_BANNED
, $exp);
146 # return value to user
147 return (defined $LJ::CONTENTFLAG_BANNED
{$value} &&
148 ($LJ::CONTENTFLAG_BANNED
{$value} == 0 || # forever
149 $LJ::CONTENTFLAG_BANNED
{$value} > time())); # not-expired
151 # cache if email_domain
152 elsif ($what eq 'email_domain'){
154 $value =~ s/^.*@//; ## in case $value is a full email address.
156 # check memcache first if not loaded
157 unless ($LJ::EMAIL_DOMAIN_BANNED_LOADED
) {
158 my $memval = LJ
::MemCache
::get
("sysban:email_domain");
160 %LJ::EMAIL_DOMAIN_BANNED
= %$memval;
161 $LJ::EMAIL_DOMAIN_BANNED_LOADED
++;
165 # is it already cached in memory?
166 if ($LJ::EMAIL_DOMAIN_BANNED_LOADED
) {
167 return (defined $LJ::EMAIL_DOMAIN_BANNED
{$value} &&
168 ($LJ::EMAIL_DOMAIN_BANNED
{$value} == 0 || # forever
169 $LJ::EMAIL_DOMAIN_BANNED
{$value} > time())); # not-expired
172 # set this now before the query
173 $LJ::EMAIL_DOMAIN_BANNED_LOADED
++;
175 LJ
::sysban_populate
(\
%LJ::EMAIL_DOMAIN_BANNED
, "email_domain")
176 or return undef $LJ::EMAIL_DOMAIN_BANNED_LOADED
;
179 my $exp = 60*15; # 15 minutes
180 LJ
::MemCache
::set
("sysban:email_domain", \
%LJ::EMAIL_DOMAIN_BANNED
, $exp);
182 # return value to user
183 return (defined $LJ::EMAIL_DOMAIN_BANNED
{$value} &&
184 ($LJ::EMAIL_DOMAIN_BANNED
{$value} == 0 || # forever
185 $LJ::EMAIL_DOMAIN_BANNED
{$value} > time())); # not-expired
189 # need the db below here
190 my $dbr = LJ
::get_db_reader
();
191 return undef unless $dbr;
193 # standard check helper
197 return $dbr->selectrow_array(qq{
200 WHERE status
= 'active'
204 AND
(NOW
() < banuntil
210 # check both ban by email and ban by domain if we have an email address
211 if ($what eq 'email') {
212 # short out if this email really is banned directly, or if we can't parse it
213 return 1 if $check->('email', $value);
214 return 0 unless $value =~ /@(.+)$/;
216 # see if this domain is banned
217 my @domains = split(/\./, $1);
219 ## invalid domain of e-mail address
220 return 1 if @domains<2;
222 ## for email like 'name@abc.def.ghi.klm',
223 ## check 'ghi.klm', 'def.ghi.klm' and 'abc.def.ghi.klm' domains
224 my $checking_domain = pop @domains;
226 $checking_domain = pop(@domains) . "." . $checking_domain;
227 return 1 if $check->('email_domain', $checking_domain);
234 # non-ip bans come straight from the db
235 return $check->($what, $value);
238 # takes a hashref to populate with 'value' => expiration pairs
239 # takes a 'what' to populate the hashref with sysbans of that type
240 # returns undef on failure, hashref on success
241 sub sysban_populate
{
242 my ($where, $what) = @_;
245 return $where if LJ
::is_enabled
('load_sysbans_from_memcache_only');
247 # call normally if no gearman/not wanted
248 return LJ
::_db_sysban_populate
($where, $what)
249 unless LJ
::conf_test
($LJ::LOADSYSBAN_USING_GEARMAN
);
251 my $gc = LJ
::gearman_client
();
252 return LJ
::_db_sysban_populate
($where, $what)
256 my $args = Storable
::nfreeze
({what
=> $what});
257 my $task = Gearman
::Task
->new("sysban_populate", \
$args,
264 my $rv = Storable
::thaw
($$res);
267 $where->{$_} = $rv->{$_} foreach keys %$rv;
270 my $ts = $gc->new_task_set();
271 $ts->add_task($task);
272 $ts->wait(timeout
=> 30); # 30 sec timeout
277 sub _db_sysban_populate
{
278 my ($where, $what) = @_;
280 my $dbh = LJ
::get_db_writer
();
281 return undef unless $dbh;
283 # build cache from db
284 my $sth = $dbh->prepare("SELECT value, UNIX_TIMESTAMP(banuntil) FROM sysban " .
285 "WHERE status='active' AND what=? " .
286 "AND NOW() > bandate " .
287 "AND (NOW() < banuntil OR banuntil IS NULL)");
288 $sth->execute($what);
289 return undef if $sth->err;
290 while (my ($val, $exp) = $sth->fetchrow_array) {
291 $where->{$val} = $exp || 0;
298 # name: LJ::sysban_note
299 # des: Inserts a properly-formatted row into [dbtable[statushistory]] noting that a ban has been triggered.
300 # args: userid?, notes, vars
301 # des-userid: The userid which triggered the ban, if available.
302 # des-notes: A very brief description of what triggered the ban.
303 # des-vars: A hashref of helpful variables to log, keys being variable name and values being values.
308 my ($userid, $notes, $vars) = @_;
311 map { $notes .= " $_=$vars->{$_};" if $vars->{$_} } sort keys %$vars;
312 LJ
::statushistory_add
($userid, 0, 'sysban_trig', $notes);
318 # name: LJ::sysban_block
319 # des: Notes a sysban in [dbtable[statushistory]] and returns a fake HTTP error message to the user.
320 # args: userid?, notes, vars
321 # des-userid: The userid which triggered the ban, if available.
322 # des-notes: A very brief description of what triggered the ban.
323 # des-vars: A hashref of helpful variables to log, keys being variable name and values being values.
328 my ($userid, $notes, $vars) = @_;
330 LJ
::sysban_note
($userid, $notes, $vars);
335 <title>503 Service Unavailable</title>
338 <h1>503 Service Unavailable</h1>
339 The service you have requested is temporarily unavailable.
344 # may not run from web context (e.g. mailgated.pl -> supportlib -> ..)
345 eval { BML
::http_response
(200, $msg); };
351 # name: LJ::sysban_create
352 # des: creates a sysban.
353 # args: what, value, bandays, note
354 # des-what: the criteria we're sysbanning on
355 # des-value: the value we're banning
356 # des-bandays: length of sysban (0 for forever)
357 # des-note: note to go with the ban (optional)
358 # info: Takes args as a hash.
359 # returns: 1 on success, 0 on failure
364 my $dbh = LJ
::get_db_writer
();
366 my $status = $opts{status
} eq 'expired' ?
'expired' : 'active';
368 my $banuntil = $opts{banuntil
} ?
$dbh->quote($opts{banuntil
}) : "NULL";
369 if ($opts{'bandays'}) {
370 $banuntil = "NOW() + INTERVAL " . $dbh->quote($opts{'bandays'}) . " DAY";
373 my $bandate = $opts{bandate
} ?
$dbh->quote($opts{bandate
}) : 'NOW()';
375 # strip out leading/trailing whitespace
376 $opts{'value'} = LJ
::trim
($opts{'value'});
379 $dbh->do("INSERT INTO sysban (status, what, value, note, bandate, banuntil) VALUES (?, ?, ?, ?, $bandate, $banuntil)",
380 undef, $status, $opts{'what'}, $opts{'value'}, $opts{'note'});
381 return $dbh->errstr if $dbh->err;
382 my $banid = $dbh->{'mysql_insertid'};
384 my $exptime = $opts{bandays
} ?
time() + 86400*$opts{bandays
} : 0;
385 # special case: creating ip/uniq ban
386 if ($opts{'what'} eq 'ip') {
387 LJ
::procnotify_add
("ban_ip", { 'ip' => $opts{'value'}, exptime
=> $exptime });
388 LJ
::MemCache
::delete("sysban:ip");
391 if ($opts{'what'} eq 'uniq') {
392 LJ
::procnotify_add
("ban_uniq", { 'uniq' => $opts{'value'}, exptime
=> $exptime});
393 LJ
::MemCache
::delete("sysban:uniq");
396 if ($opts{'what'} eq 'contentflag') {
397 LJ
::procnotify_add
("ban_contentflag", { 'username' => $opts{'value'}, exptime
=> $exptime});
398 LJ
::MemCache
::delete("sysban:contentflag");
401 if ($opts{'what'} eq 'ip_captcha'){
402 LJ
::CaptchaServer
->ban_ip($opts{'value'});
405 # log in statushistory
406 my $remote = LJ
::get_remote
();
407 $banuntil = $opts{'bandays'} ? LJ
::TimeUtil
->mysql_time($exptime) : "forever";
409 LJ
::statushistory_add
(0, $remote || 0, 'sysban_add',
410 "banid=$banid; status=$status; " .
411 "bandate=" . LJ
::TimeUtil
->mysql_time() . "; banuntil=$banuntil; " .
412 "what=$opts{'what'}; value=$opts{'value'}; " .
413 "note=$opts{'note'};");
420 # name: LJ::sysban_validate
421 # des: determines whether a sysban can be added for a given value.
423 # des-type: the sysban type we're checking
424 # des-value: the value we're checking
425 # returns: nothing on success, error message on failure
427 sub sysban_validate
{
428 my ($what, $value, $opts) = @_;
430 # bail early if the ban already exists
431 return "This is already banned"
432 if !$opts->{skipexisting
} && LJ
::sysban_check
($what, $value, $opts);
434 my $ip_regexp = qr/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/;
435 my $ip_to_str = sub { return pack("C4",split(/\./, $_[0])); };
441 return "Format: xxx.xxx.xxx.xxx (ip address)"
442 unless $ip =~ /^$ip_regexp(?:\/\d
+)?
$/;
444 my $tmp = new Net
::IP
($ip) or
445 return Net
::IP
::Error
();
447 while (my ($ip_re, $reason) = each %LJ::UNBANNABLE_IPS
) {
448 next unless $ip =~ $ip_re;
449 return "Cannot ban IP $ip: " . LJ
::ehtml
($reason);
452 ## LJ::sysban_populate() doesn't return notes, so select them from DB
453 my $dbh = LJ
::get_db_reader
()
454 or die "Can't connect to db reader";
455 my $whitelist = $dbh->selectall_arrayref(
459 WHERE what = 'ip_whitelist'
460 AND status = 'active'
462 AND (NOW() < banuntil
469 ## if creting a new ban, check IP whitelist
470 ## TODO: when modifying an existing ban, give a warning
471 if (!$opts->{'skipexisting'}) {
473 foreach my $wl (@
$whitelist) {
474 my $mask = $wl->{value
}; ## see ip_whitelist below for possible formats
475 if ($mask =~ /^$ip_regexp$/) {
480 } elsif (my ($start_ip, $end_ip) = $mask =~ /^($ip_regexp)-($ip_regexp)$/) {
481 if ( $ip_to_str->($start_ip) le $ip_to_str->($ip) &&
482 $ip_to_str->($ip) le $ip_to_str->($end_ip))
487 } elsif ($mask =~ m!^$ip_regexp/(\d+)!) {
488 my $netmask = Net
::Netmask
->new($mask);
489 if ($netmask->match($ip)) {
493 } elsif ($mask =~ /^(\d+\.){1,3}\*$/) {
496 if ($ip =~ /^$mask$/) {
506 return "Can't ban ip address $ip: ip_whitelist #$matched_wl->{banid} matched ($matched_wl->{note})";
515 return $uniq =~ /^[a-zA-Z0-9]{15}$/ ?
516 0 : "Invalid uniq: must be 15 digits/chars";
522 LJ
::check_email
($email, \
@err);
523 return @err ?
shift @err : 0;
525 'email_domain' => sub {
526 my $email_domain = shift;
528 if ($email_domain =~ /^[^@]+\.[^@]+$/) {
531 return "Invalid email domain: $email_domain";
537 my $u = LJ
::load_user
($user);
538 return $u ?
0 : "Invalid user: $user";
543 return $cc =~ /^\d{4}-\d{4}$/ ?
544 0 : "Format: xxxx-xxxx (first four-last four)";
549 return $num =~ /\d{10}/ ?
0 : 'Format: 10 digit MSISDN';
552 'ip_whitelist' => sub {
556 ## allowed formats: exact IP address, range IP1-IP2, subnet: IP/num, mask: 123.456.*
557 if ( $mask =~ /^$ip_regexp$/ ||
558 $mask =~ /^$ip_regexp-$ip_regexp$/ ||
559 $mask =~ m!^$ip_regexp/\d+$! ||
560 $mask =~ /^(\d+\.){1,3}\*$/ )
564 return "Format: xxx.xxx.xxx.xxx (exact IP address), " .
565 "xxx.xxx.xxx.xxx-yyy.yyy.yyy.yyy (IP range), " .
566 "xxx.xxx.xxx.xxx/yyy (subnet) or " .
572 # aliases to handlers above
573 my @map = ('pay_user' => 'user',
574 'pay_email' => 'email',
575 'pay_uniq' => 'uniq',
576 'support_user' => 'user',
577 'support_email' => 'email',
578 'support_uniq' => 'uniq',
579 'lostpassword' => 'user',
580 'lostpassword_email' => 'email',
581 'talk_ip_test' => 'ip',
582 'contentflag' => 'user',
583 'ip_captcha' => 'ip',
586 while (my ($new, $existing) = splice(@map, 0, 2)) {
587 $validate->{$new} = $validate->{$existing};
590 my $check = $validate->{$what} or return "Invalid sysban type";
591 return $check->($value);