pb4sd 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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. interval => 1,
  92. adjustafter => 3,
  93. maxinterval => 2,
  94. resetafter => 30,
  95. ignore_nonexistant => 1,
  96. tail => 0,
  97. reset_tail => -1
  98. ) || die "unable to create tail object for '$infile'";
  99. # create network block
  100. my $nt = {};
  101. foreach my $exclude (@exclude) {
  102. my $nb = new Net::Netmask ($exclude) || die;
  103. $nb->storeNetblock($nt);
  104. }
  105. # create DB hash file
  106. my %db;
  107. my $dbh = tie %db, 'DB_File', $dbfile, O_CREAT|O_RDWR, 0666, $DB_HASH
  108. || die "cannot open DB file '$dbfile': $!\n";
  109. # create DB hash file descriptor
  110. my $fd = $dbh->fd;
  111. open(DB_FH, "+<&=$fd") || die "cannot open '$dbfile' filehandle: $!\n";
  112. # delete database
  113. flock(DB_FH, LOCK_EX) || die "(exclusive) lock failed: $!\n";
  114. foreach $k (keys(%db)) {
  115. delete $db{$k};
  116. }
  117. flock(DB_FH, LOCK_UN) or die "unlock failed: $!\n";
  118. # open logfile
  119. my $log = new IO::File ">>$logfile" || die;
  120. $log->autoflush(1);
  121. # establish signal handlers
  122. $SIG{__DIE__} = sub {
  123. $log->print("[".localtime(time())."] DIE error=".join(" ", @_)."\n");
  124. die @_;
  125. };
  126. # start/stop logging
  127. $log->print("[".localtime(time())."] STARTUP\n");
  128. $SIG{'QUIT'} = $SIG{'INT'} = $SIG{'TERM'} = sub {
  129. $log->print("[".localtime(time())."] SHUTDOWN\n");
  130. exit(0);
  131. };
  132. # optionally daemonize
  133. if ($daemon) {
  134. my ($pid, $sess_id, $i);
  135. # fork and exit parent
  136. if ($pid = fork()) {
  137. exit(0);
  138. }
  139. # detach from the terminal
  140. $sess_id = POSIX::setsid();
  141. # prevent possibility of acquiring a controling terminal
  142. $SIG{'HUP'} = 'IGNORE';
  143. if ($pid = fork()) {
  144. exit(0);
  145. }
  146. # create pidfile
  147. open(PID, ">$pidfile") || die;
  148. printf(PID "%d\n", POSIX::getpid());
  149. close(PID);
  150. # change working directory
  151. chdir("/");
  152. # clear file creation mask
  153. umask(0);
  154. # close stdio file descriptors
  155. close(STDIN);
  156. close(STDOUT);
  157. close(STDERR);
  158. # re-open stdio file descriptors to /dev/null
  159. open(STDIN, "+>/dev/null");
  160. open(STDOUT, "+>&STDIN");
  161. open(STDERR, "+>&STDIN");
  162. }
  163. my $t = {}; # ip to expire table
  164. my $q = []; # ip/expire stack
  165. while (1) {
  166. my $line = $lf->read();
  167. my $now = time();
  168. if ($line =~ m/$pattern->{$popserver}/o) {
  169. my ($timestamp, $ipaddr) = ($1, $2);
  170. # log recognition of entry
  171. $log->print("[".localtime($now)."] SEE client=".$ipaddr.
  172. " login=".localtime(str2time($timestamp))."\n");
  173. # calculate expire time
  174. my $expire = str2time($timestamp) || next;
  175. $expire += $grace;
  176. # skip if grace period is already expired or ip is excluded
  177. next if ($expire < $now);
  178. next if (findNetblock($ipaddr, $nt));
  179. # push ip/expire onto stack
  180. push @{$q}, [$ipaddr, $expire];
  181. # remember ip
  182. my $already_enabled = exists($t->{$ipaddr});
  183. $t->{$ipaddr} = $expire;
  184. # skip if ip was already enabled
  185. if ($already_enabled) {
  186. $log->print("[".localtime($now)."] UPD client=".$ipaddr." logout=".localtime($expire)."\n");
  187. next;
  188. }
  189. # lock database
  190. flock(DB_FH, LOCK_EX);
  191. # add entry to database
  192. $db{$ipaddr} = "OK";
  193. $log->print("[".localtime($now)."] ADD client=".$ipaddr." logout=".localtime($expire)."\n");
  194. # purge expired database entries
  195. while ($q->[0][1] < $now) {
  196. if ($q->[0][1] == $t->{$q->[0][0]}) {
  197. $log->print("[".localtime($now)."] DEL client=".$q->[0][0]." logout=".localtime($q->[0][1])."\n");
  198. delete $t->{$q->[0][0]};
  199. delete $db{$q->[0][0]};
  200. }
  201. shift @{$q};
  202. }
  203. # synchronize database
  204. $dbh->sync();
  205. # unlock database
  206. flock(DB_FH, LOCK_UN);
  207. }
  208. }
  209. __DATA__
  210. =pod
  211. =head1 NAME
  212. pb4sd -- POP-before-SMTP Daemon
  213. =head1 SYNOPSIS
  214. B<p4bsd>
  215. [--daemon]
  216. [--infile=filename]
  217. [--dbfile=filename]
  218. [--popserver=type]
  219. [--exclude=a.b.c.d/x]
  220. [--grace=seconds]
  221. [--logfile=filename]
  222. [--pidfile=filename]
  223. =head1 DESCRIPTION
  224. B<pb4sd> is a little daemon program which watches a POP/IMAP server's
  225. logfile for successful client authentications and writes the
  226. corresponding IP addresses into a Berkeley-DB hash file. This hash file
  227. then can be used by the MTA to allow relaying access.
  228. For debugging purposes you can dump the generated hash file with
  229. Berkeley-DB's C<db_dump -p> and query it selectively via Postfix's
  230. C<postmap -q>.
  231. =cut