#!/usr/bin/perl -w # This is a pwcheck daemon for checking Cyrus IMAP with SASL logins against # a database. # # You need a table with a User name column (constant USERFLD) and a # password column (constant PASSWORDFLD). # # Passwords can be inserted using MySQL's PASSWORD() function to encrypt them, # or using builtin MD5 (see constant USE_MD5 below). # # You need to put 3 params in a file called /etc/authcheck.conf: # databasename user password tablename # Note that each param should be on a separate line (not like here!) # The 1st param is a DBI connection string, eg. dbi:mysql:dbname:host # # Also see the comments with constant USE_MD5 (below). # # This is loosely based on code attributed as: # authcheck, (C) Vladimir Ivaschenko , GPL2 license # http://www.hazard.maks.net/~hazard # # The preforking server code is from The Perl Cookbook (O'Reilly), Recipe 17.12. # # The remaining code is (C) Jeremy Howard , GPL2 license # http://www.fastmail.fm # # Mon 10 Jul 2000 11:51:44 AM EEST: Vladimir Ivaschenko # - some changes to backward compatibility with old authcheck # - daemonize # # TODO: # --Add POD documentation # --Deal with clients timing out better use strict; use IO::Socket; use Symbol; use DBI; use Digest::MD5 qw(md5_base64); use Unix::Syslog qw(:macros :subs); use POSIX; use constant PATH=>'/var/pwcheck/pwcheck'; use constant PIDPATH=>'/var/lock/authcheck.pid'; use constant PASSWORDFLD=>'password'; use constant USERFLD=>'login'; use constant CONFIGFILE=>'/etc/authcheck.conf'; # USE_MD5 is 1 if passwords are scrambled with md5_base64 (perl function), or # 0 (zero) if passwords are scrambled with MySQL's PASSWORD() function. use constant USE_MD5=>1; #------------------------------------------------------------ # global variables my $PREFORK = 5; # number of children to maintain my $MAX_CLIENTS_PER_CHILD = 100; # number of clients each child should process my %children = (); # keys are current child process IDs my $children = 0; # current number of children #------------------------------------------------------------ # Grab login params from command line open (CONFIG, '<'.CONFIGFILE); chomp (my ($D_DSN, $D_LOGIN, $D_PASSWORD, $D_TABLE) = ); close CONFIG; #------------------------------------------------------------ # Create database and statement handlers if not already done my ($dbh,$sth) = undef; sub dbConnect { # Create connection to database. # This doesn't do anything if already connected. if (!$dbh || $DBI::errstr || !$dbh->{Active}) { Unix::Syslog::syslog LOG_INFO, "Connecting to database", 0; $sth->finish() if $sth; $dbh->disconnect() if $dbh; $dbh = undef; # Some drivers (e.g. DBD::Sybase) need this $dbh = DBI->connect ($D_DSN, $D_LOGIN, $D_PASSWORD); # Create statement handle # The SQL gets the encrypted version of user's claimed password, and # the actual encrypted version stored in database. If the user doesn't # exist, no rows will be returned. $sth = $dbh->prepare( 'SELECT PASSWORD(?), '.PASSWORDFLD.' FROM '.$D_TABLE.' WHERE '.USERFLD.'=?' ) if $dbh; } # If anything didn't work, wait a while and try again if ($DBI::errstr) { Unix::Syslog::syslog LOG_ERR, "No DB connection--reconnecting", 0; sleep 10; # Avoids recursion with a 'goto' (I think)--avoid filling up stack space goto &dbConnect; } } #------------------------------------------------------------ # Get the password corresponding with this user sub GetRows { my $username=$_[0]; my $password=$_[1]; my @rows=undef; dbConnect(); # Try and exec the query. If we can't, we've probably lost our DB connection... while (!$sth->execute($username, $password)) { # ... so wait a while and get it back. sleep 10; dbConnect(); } if (defined $DBI::errstr) { Unix::Syslog::syslog LOG_ERR, $DBI::errstr; return (); } @rows=$sth->fetchrow_array; return @rows; } #------------------------------------------------------------ # Set up socket Unix::Syslog::openlog("authcheck", LOG_PID | LOG_CONS, LOG_DAEMON); unlink (PATH); # Save current default permissions for this process, and remove default # permissions before creating socket my $oldumask = umask(0); my $listen = undef; # Try and listen on socket defined by PATH if (!($listen = IO::Socket::UNIX->new( Type=>SOCK_STREAM, Local=>PATH, Listen=>0))) { Unix::Syslog::syslog LOG_ERR, "Could not open listen socket.", 0; die "Could not open listen socket."; } # Restore this process's permissions umask($oldumask); # Record separator = empty lines (see perldoc perlvar) $/ = "\000"; #------------------------------------------------------------ # takes care of dead children sub REAPER { $SIG{CHLD} = \&REAPER; my $pid = wait; $children --; delete $children{$pid}; $sth->finish() if $sth; $dbh->disconnect() if $dbh; } #------------------------------------------------------------ # signal handler for SIGINT sub HUNTSMAN { local($SIG{CHLD}) = 'IGNORE'; # we're going to kill our children unlink (PATH); unlink (PIDPATH); syslog LOG_ERR, "Exiting on INT signal."; kill 'INT' => keys %children; exit; # clean up with dignity } # daemonize if (fork()) { exit(0); } open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; open STDOUT, '>/dev/null'; open STDERR, '>/dev/null'; # Change to root dir to avoid locking a mounted file system chdir '/' or die "Can't chdir to /: $!"; # Turn process into session leader, and ensure no controlling terminal POSIX::setsid(); # Fork off our children. for (1 .. $PREFORK) { make_new_child(); } # Install signal handlers. $SIG{CHLD} = \&REAPER; $SIG{INT} = \&HUNTSMAN; # And maintain the population. while (1) { sleep; # wait for a signal (i.e., child's death) for (my $i = $children; $i < $PREFORK; $i++) { make_new_child(); # top up the child pool } } sub make_new_child { my $pid; my $sigset; # block signal for fork $sigset = POSIX::SigSet->new(SIGINT); sigprocmask(SIG_BLOCK, $sigset) or die "Can't block SIGINT for fork: $!\n"; die "fork: $!" unless defined ($pid = fork); if ($pid) { # Parent records the child's birth and returns. sigprocmask(SIG_UNBLOCK, $sigset) or die "Can't unblock SIGINT for fork: $!\n"; $children{$pid} = 1; $children++; return; } else { # Child can *not* return from this subroutine. $SIG{INT} = 'DEFAULT'; # make SIGINT kill us as it did before # unblock signals sigprocmask(SIG_UNBLOCK, $sigset) or die "Can't unblock SIGINT for fork: $!\n"; # handle connections until we've reached $MAX_CLIENTS_PER_CHILD for (my $i=0; $i < $MAX_CLIENTS_PER_CHILD; $i++) { my $sock = $listen->accept() or last; # do the actual work! CheckPassword($sock); } # tidy up gracefully and finish # this exit is VERY important, otherwise the child will become # a producer of more and more children, forking yourself into # process death. exit; } } sub CheckPassword { my ($sock) = @_; my $username=$sock->getline || return undef; chop $username; my $password=$sock->getline || return undef; chop $password; my @rows = GetRows($password, $username); # @rows now contains (Encrypted ver of curr password, encrypted # version of actual password), or is empty if the user doesn't exist. if ($#rows>=0 && (USE_MD5 ? $rows[1] eq md5_base64($password) # Use md5_base64 scrambled pass : $rows[1] eq $rows[0])) { # Use MySQL PASSWORD() # syslog LOG_INFO, "success $username $password"; $sock->printflush("OK\000"); } else { # syslog LOG_INFO, "fail $username $password"; $sock->printflush("Incorrect password\000"); } # Do we need to close the socket? Perl Cookbook doesn't... $sock->close; }