#!/usr/bin/perl ## ## pb4sd -- POP-before-SMTP Daemon ## Copyright (c) 2001 Ralf S. Engelschall ## ## This program is derived from Bennett Todd 's ## pop-before-smtp 1.28 (http://people.oven.com/bet/pop-before-smtp/) ## use File::Tail; use DB_File; use Net::Netmask; use Date::Parse; use Getopt::Long; use Fcntl ':flock'; use POSIX qw(getpid setsid); use IO; # logfile parsing patters my $pattern = { # QPopper 4.0.x (OpenPKG) 'qpopper' => '^(... .. ..:..:..) (?:<\S+>|\S+) (?:/\S+?)?q?popper\S*\[\d+\]: ' . '\([^)]*\) POP login by user "[^"]+" at \([^)]+\) (\d+.\d+.\d+.\d+)$', # Qpopper 3.x 'popper3' => '^(\w{3} \w{3} \d{2} \d{2}:\d{2}:\d{2} \d{4}) \[\d+\] ' . ' Stats:\s+\w+ \d \d \d \d [\w\.]+ (\d+\.\d+\.\d+\.\d+)', # UW ipop3d/imapd 'ipop3d' => '^(... .. ..:..:..) \S+ (?:ipop3d|imapd)\[\d+\]: ' . '(?:Login|Authenticated|Auth) user=\S+ host=(?:\S+)?\[(\d+\.\d+\.\d+\.\d+)\](?: nmsgs=\d+/\d+)?$', # GNU pop3d 'popd3d' => '^(... .. ..:..:..) \S+ gnu-pop3d\[\d+\]: ' . 'User .* logged in with mailbox .* from (\d+\.\d+\.\d+\.\d+)$', # Cyrus 'cyrus' => '^(... .. ..:..:..) \S+ (?:pop3d|imapd)\[\d+\]: ' . 'login: \S*\[(\d+\.\d+\.\d+\.\d+)\] \S+ \S+', # Courier-IMAP 'courier' => '^(... .. ..:..:..) \S+ imaplogin: ' . 'LOGIN, user=\S+, ip=\[(\d+\.\d+\.\d+\.\d+)\]$', # Qmail pop3d 'pop3d' => '^(... .. ..:..:..) \S+ vpopmail\[\d+\]: ' . 'vchkpw: login \[\S+\] from (\d+\.\d+\.\d+\.\d+)$', # cucipop 'cucipop' => '^(... .. ..:..:..) \S+ cucipop\[\d+\]: \S+ ' . '(\d+\.\d+\.\d+\.\d+) \d+, \d+ \(\d+\), \d+ \(\d+\)', # popa3d 'popa3d' => '^(... .. ..:..:..) \S+ popa3d\[\d+\]: Authentication passed for \S+ -- \[(\d+.\d+.\d+.\d+)\]$', }; # parameters and their defaults my $daemon = 0; my $infile = 'qpopper.log'; my $dbfile = 'qpopper'; my $popserver = 'qpopper'; my @exclude = (); my $grace = 1800; my $logfile = 'pb4s.log'; my $pidfile = 'pb4s.pid'; # option parsing GetOptions( "daemon!" => \$daemon, "infile=s" => \$infile, "dbfile=s" => \$dbfile, "popserver=s" => \$popserver, "exclude=s@" => \@exclude, "grace=i" => \$grace, "logfile=s" => \$logfile, "pidfile=s" => \$pidfile, ) or die "Usage: p4bs [--daemon]\n" . " [--infile=filename]\n" . " [--dbfile=filename]\n" . " [--popserver=type]\n" . " [--exclude=a.b.c.d/x]\n" . " [--grace=seconds]\n" . " [--logfile=filename]\n" . " [--pidfile=filename]\n"; # make sure filenames are specified as absolute paths die "--infile requires absolute filename" if ($infile !~ m|^/|); # make sure it is a known pop server die "unknown pop server '$popserver'" if (not defined($pattern->{$popserver})); # make sure input logfile exists die "logfile '$infile' not found" if (not -f $infile); # create tail object my $lf = File::Tail->new( name => $infile, maxinterval => 2, interval => 1, adjustafter => 3, resetafter => 30 ) || die "unable to create tail object for '$infile'"; # create network block my $nt = {}; foreach my $exclude (@exclude) { my $nb = new Net::Netmask ($exclude) || die; $nb->storeNetblock($nt); } # create DB hash file my %db; my $dbh = tie %db, 'DB_File', $dbfile, O_CREAT|O_RDWR, 0666, $DB_HASH || die "cannot open DB file '$dbfile': $!\n"; # create DB hash file descriptor my $fd = $dbh->fd; open(DB_FH, "+<&=$fd") || die "cannot open '$dbfile' filehandle: $!\n"; # delete database flock(DB_FH, LOCK_EX) || die "(exclusive) lock failed: $!\n"; foreach $k (keys(%db)) { delete $db{$k}; } flock(DB_FH, LOCK_UN) or die "unlock failed: $!\n"; # open logfile my $log = new IO::File ">>$logfile" || die; $log->autoflush(1); # establish signal handlers $SIG{__DIE__} = sub { $log->print("[".localtime(time())."] DIE error=".join(" ", @_)."\n"); die @_; }; # start/stop logging $log->print("[".localtime(time())."] STARTUP\n"); $SIG{'QUIT'} = $SIG{'INT'} = $SIG{'TERM'} = sub { $log->print("[".localtime(time())."] SHUTDOWN\n"); exit(0); }; # optionally daemonize if ($daemon) { my ($pid, $sess_id, $i); # fork and exit parent if ($pid = fork()) { exit(0); } # detach from the terminal $sess_id = POSIX::setsid(); # prevent possibility of acquiring a controling terminal $SIG{'HUP'} = 'IGNORE'; if ($pid = fork()) { exit(0); } # create pidfile open(PID, ">$pidfile") || die; printf(PID "%d\n", POSIX::getpid()); close(PID); # change working directory chdir("/"); # clear file creation mask umask(0); # close stdio file descriptors close(STDIN); close(STDOUT); close(STDERR); # re-open stdio file descriptors to /dev/null open(STDIN, "+>/dev/null"); open(STDOUT, "+>&STDIN"); open(STDERR, "+>&STDIN"); } my $t = {}; # ip to expire table my $q = []; # ip/expire stack while (1) { my $line = $lf->read(); my $now = time(); if ($line =~ m/$pattern->{$popserver}/o) { my ($timestamp, $ipaddr) = ($1, $2); # log recognition of entry $log->print("[".localtime($now)."] SEE client=".$ipaddr. " login=".localtime(str2time($timestamp))."\n"); # calculate expire time my $expire = str2time($timestamp) || next; $expire += $grace; # skip if grace period is already expired or ip is excluded next if ($expire < $now); next if (findNetblock($ipaddr, $nt)); # push ip/expire onto stack push @{$q}, [$ipaddr, $expire]; # remember ip my $already_enabled = exists($t->{$ipaddr}); $t->{$ipaddr} = $expire; # skip if ip was already enabled next if $already_enabled; # lock database flock(DB_FH, LOCK_EX); # add entry to database $db{$ipaddr} = "OK"; $log->print("[".localtime($now)."] ADD client=".$ipaddr." logout=".localtime($expire)."\n"); # purge expired database entries while ($q->[0][1] < $now) { if ($q->[0][1] == $t->{$q->[0][0]}) { $log->print("[".localtime($now)."] DEL client=".$q->[0][0]." logout=".localtime($q->[0][1])."\n"); delete $t->{$q->[0][0]}; delete $db{$q->[0][0]}; } shift @q; } # synchronize database $dbh->sync(); # unlock database flock(DB_FH, LOCK_UN); } } __DATA__ =pod =head1 NAME pb4sd -- POP-before-SMTP Daemon =head1 SYNOPSIS B [--daemon] [--infile=filename] [--dbfile=filename] [--popserver=type] [--exclude=a.b.c.d/x] [--grace=seconds] [--logfile=filename] [--pidfile=filename] =head1 DESCRIPTION B is a little daemon program which watches a POP/IMAP server's logfile for successful client authentications and writes the corresponding IP addresses into a Berkeley-DB hash file. This hash file then can be used by the MTA to allow relaying access. =cut