pb4sd 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. #!/usr/bin/perl
  2. ##
  3. ## pb4sd -- POP-before-SMTP Daemon
  4. ## Copyright (c) 2001 Ralf S. Engelschall <rse@engelschall.com>
  5. ##
  6. ## This program is derived from Bennett Todd <bet@rahul.net>'s
  7. ## pop-before-smtp 1.28 (http://people.oven.com/bet/pop-before-smtp/)
  8. ##
  9. use File::Tail;
  10. use DB_File;
  11. use Net::Netmask;
  12. use Date::Parse;
  13. use Getopt::Long;
  14. use Fcntl ':flock';
  15. use POSIX qw(getpid setsid);
  16. use IO;
  17. # logfile parsing patters
  18. my $pattern = {
  19. # QPopper 4.0.x (OpenPKG)
  20. 'qpopper' =>
  21. '^(... .. ..:..:..) (?:<\S+>|\S+) (?:/\S+?)?q?popper\S*\[\d+\]: ' .
  22. '\([^)]*\) POP login by user "[^"]+" at \([^)]+\) (\d+.\d+.\d+.\d+)$',
  23. # Qpopper 3.x
  24. 'popper3' =>
  25. '^(\w{3} \w{3} \d{2} \d{2}:\d{2}:\d{2} \d{4}) \[\d+\] ' .
  26. ' Stats:\s+\w+ \d \d \d \d [\w\.]+ (\d+\.\d+\.\d+\.\d+)',
  27. # UW ipop3d/imapd
  28. 'ipop3d' =>
  29. '^(... .. ..:..:..) \S+ (?:ipop3d|imapd)\[\d+\]: ' .
  30. '(?:Login|Authenticated|Auth) user=\S+ host=(?:\S+)?\[(\d+\.\d+\.\d+\.\d+)\](?: nmsgs=\d+/\d+)?$',
  31. # GNU pop3d
  32. 'popd3d' =>
  33. '^(... .. ..:..:..) \S+ gnu-pop3d\[\d+\]: ' .
  34. 'User .* logged in with mailbox .* from (\d+\.\d+\.\d+\.\d+)$',
  35. # Cyrus
  36. 'cyrus' =>
  37. '^(... .. ..:..:..) \S+ (?:pop3d|imapd)\[\d+\]: ' .
  38. 'login: \S*\[(\d+\.\d+\.\d+\.\d+)\] \S+ \S+',
  39. # Courier-IMAP
  40. 'courier' =>
  41. '^(... .. ..:..:..) \S+ imaplogin: ' .
  42. 'LOGIN, user=\S+, ip=\[(\d+\.\d+\.\d+\.\d+)\]$',
  43. # Qmail pop3d
  44. 'pop3d' =>
  45. '^(... .. ..:..:..) \S+ vpopmail\[\d+\]: ' .
  46. 'vchkpw: login \[\S+\] from (\d+\.\d+\.\d+\.\d+)$',
  47. # cucipop
  48. 'cucipop' =>
  49. '^(... .. ..:..:..) \S+ cucipop\[\d+\]: \S+ ' .
  50. '(\d+\.\d+\.\d+\.\d+) \d+, \d+ \(\d+\), \d+ \(\d+\)',
  51. # popa3d
  52. 'popa3d' =>
  53. '^(... .. ..:..:..) \S+ popa3d\[\d+\]: Authentication passed for \S+ -- \[(\d+.\d+.\d+.\d+)\]$',
  54. };
  55. # parameters and their defaults
  56. my $daemon = 0;
  57. my $infile = 'qpopper.log';
  58. my $dbfile = 'qpopper';
  59. my $popserver = 'qpopper';
  60. my @exclude = ();
  61. my $grace = 1800;
  62. my $logfile = 'pb4s.log';
  63. my $pidfile = 'pb4s.pid';
  64. # option parsing
  65. GetOptions(
  66. "daemon!" => \$daemon,
  67. "infile=s" => \$infile,
  68. "dbfile=s" => \$dbfile,
  69. "popserver=s" => \$popserver,
  70. "exclude=s@" => \@exclude,
  71. "grace=i" => \$grace,
  72. "logfile=s" => \$logfile,
  73. "pidfile=s" => \$pidfile,
  74. ) or die "Usage: p4bs [--daemon]\n" .
  75. " [--infile=filename]\n" .
  76. " [--dbfile=filename]\n" .
  77. " [--popserver=type]\n" .
  78. " [--exclude=a.b.c.d/x]\n" .
  79. " [--grace=seconds]\n" .
  80. " [--logfile=filename]\n" .
  81. " [--pidfile=filename]\n";
  82. # make sure filenames are specified as absolute paths
  83. die "--infile requires absolute filename" if ($infile !~ m|^/|);
  84. # make sure it is a known pop server
  85. die "unknown pop server '$popserver'" if (not defined($pattern->{$popserver}));
  86. # make sure input logfile exists
  87. die "logfile '$infile' not found" if (not -f $infile);
  88. # create tail object
  89. my $lf = File::Tail->new(
  90. name => $infile,
  91. maxinterval => 2,
  92. interval => 1,
  93. adjustafter => 3,
  94. resetafter => 30
  95. ) || die "unable to create tail object for '$infile'";
  96. # create network block
  97. my $nt = {};
  98. foreach my $exclude (@exclude) {
  99. my $nb = new Net::Netmask ($exclude) || die;
  100. $nb->storeNetblock($nt);
  101. }
  102. # create DB hash file
  103. my %db;
  104. my $dbh = tie %db, 'DB_File', $dbfile, O_CREAT|O_RDWR, 0666, $DB_HASH
  105. || die "cannot open DB file '$dbfile': $!\n";
  106. # create DB hash file descriptor
  107. my $fd = $dbh->fd;
  108. open(DB_FH, "+<&=$fd") || die "cannot open '$dbfile' filehandle: $!\n";
  109. # delete database
  110. flock(DB_FH, LOCK_EX) || die "(exclusive) lock failed: $!\n";
  111. foreach $k (keys(%db)) {
  112. delete $db{$k};
  113. }
  114. flock(DB_FH, LOCK_UN) or die "unlock failed: $!\n";
  115. # open logfile
  116. my $log = new IO::File ">>$logfile" || die;
  117. $log->autoflush(1);
  118. # establish signal handlers
  119. $SIG{__DIE__} = sub {
  120. $log->print("[".localtime(time())."] DIE error=".join(" ", @_)."\n");
  121. die @_;
  122. };
  123. # start/stop logging
  124. $log->print("[".localtime(time())."] STARTUP\n");
  125. $SIG{'QUIT'} = $SIG{'INT'} = $SIG{'TERM'} = sub {
  126. $log->print("[".localtime(time())."] SHUTDOWN\n");
  127. exit(0);
  128. };
  129. # optionally daemonize
  130. if ($daemon) {
  131. my ($pid, $sess_id, $i);
  132. # fork and exit parent
  133. if ($pid = fork()) {
  134. exit(0);
  135. }
  136. # detach from the terminal
  137. $sess_id = POSIX::setsid();
  138. # prevent possibility of acquiring a controling terminal
  139. $SIG{'HUP'} = 'IGNORE';
  140. if ($pid = fork()) {
  141. exit(0);
  142. }
  143. # create pidfile
  144. open(PID, ">$pidfile") || die;
  145. printf(PID "%d\n", POSIX::getpid());
  146. close(PID);
  147. # change working directory
  148. chdir("/");
  149. # clear file creation mask
  150. umask(0);
  151. # close stdio file descriptors
  152. close(STDIN);
  153. close(STDOUT);
  154. close(STDERR);
  155. # re-open stdio file descriptors to /dev/null
  156. open(STDIN, "+>/dev/null");
  157. open(STDOUT, "+>&STDIN");
  158. open(STDERR, "+>&STDIN");
  159. }
  160. my $t = {}; # ip to expire table
  161. my $q = []; # ip/expire stack
  162. while (1) {
  163. my $line = $lf->read();
  164. my $now = time();
  165. if ($line =~ m/$pattern->{$popserver}/o) {
  166. my ($timestamp, $ipaddr) = ($1, $2);
  167. # log recognition of entry
  168. $log->print("[".localtime($now)."] SEE client=".$ipaddr.
  169. " login=".localtime(str2time($timestamp))."\n");
  170. # calculate expire time
  171. my $expire = str2time($timestamp) || next;
  172. $expire += $grace;
  173. # skip if grace period is already expired or ip is excluded
  174. next if ($expire < $now);
  175. next if (findNetblock($ipaddr, $nt));
  176. # push ip/expire onto stack
  177. push @{$q}, [$ipaddr, $expire];
  178. # remember ip
  179. my $already_enabled = exists($t->{$ipaddr});
  180. $t->{$ipaddr} = $expire;
  181. # skip if ip was already enabled
  182. next if $already_enabled;
  183. # lock database
  184. flock(DB_FH, LOCK_EX);
  185. # add entry to database
  186. $db{$ipaddr} = "OK";
  187. $log->print("[".localtime($now)."] ADD client=".$ipaddr." logout=".localtime($expire)."\n");
  188. # purge expired database entries
  189. while ($q->[0][1] < $now) {
  190. if ($q->[0][1] == $t->{$q->[0][0]}) {
  191. $log->print("[".localtime($now)."] DEL client=".$q->[0][0]." logout=".localtime($q->[0][1])."\n");
  192. delete $t->{$q->[0][0]};
  193. delete $db{$q->[0][0]};
  194. }
  195. shift @q;
  196. }
  197. # synchronize database
  198. $dbh->sync();
  199. # unlock database
  200. flock(DB_FH, LOCK_UN);
  201. }
  202. }
  203. __DATA__
  204. =pod
  205. =head1 NAME
  206. pb4sd -- POP-before-SMTP Daemon
  207. =head1 SYNOPSIS
  208. B<p4bsd>
  209. [--daemon]
  210. [--infile=filename]
  211. [--dbfile=filename]
  212. [--popserver=type]
  213. [--exclude=a.b.c.d/x]
  214. [--grace=seconds]
  215. [--logfile=filename]
  216. [--pidfile=filename]
  217. =head1 DESCRIPTION
  218. B<pb4sd> is a little daemon program which watches a POP/IMAP server's
  219. logfile for successful client authentications and writes the
  220. corresponding IP addresses into a Berkeley-DB hash file. This hash file
  221. then can be used by the MTA to allow relaying access.
  222. =cut