Thanks to Martin who compiles this. I took no credit for this article.
Please follow the new Qmailrocks guide at http://qmailrocks.thibs.com
Because vpopmail is configured to use MySQL, poppassd does not work anymore.
poppassd is need for the Change Password plugin in SquirrelMail.
To add back the "Change Password" function in SquirrelMail, please do the following (based on http://www-rohan.sdsu.edu/~cleaver/software/qmail/).
1. nano /usr/bin/vchkpassd
Because vpopmail is configured to use MySQL, poppassd does not work anymore.
poppassd is need for the Change Password plugin in SquirrelMail.
To add back the "Change Password" function in SquirrelMail, please do the following (based on http://www-rohan.sdsu.edu/~cleaver/software/qmail/).
1. nano /usr/bin/vchkpassd
#!/usr/bin/perl
# This program functions as a poppassd (port 106) daemon for use with
# MySQL-based qmail/vpopmail setups. It reads directly out of the
# vpopmail table in MySQL to verify passwords, but writes back using
# vpasswd.
# It includes some basic security features, including:
# - tarpitting (3 second delays on failure)
# - IP address stopping (connection must be from something in
# the lastauth table to be accepted)
# - uses vpasswd command to ensure encrypted pw placed in
#
# v 1.0 - Japheth Cleaver
# cleaver@rohan.sdsu.edu
#
# No caching for us.
$|=1;
use DBI;
use Socket qw(:DEFAULT :crlf);
local $/=$LF;
##########################################################################
sub quitNice {
print '200 Ja ne!', $CRLF;
close STDOUT;
exit;
};
sub quitError {
my $error = shift || 'An unknown error occurred';
print '500 ', $error, $CRLF;
close STDOUT;
exit;
};
##########################################################################
BEGIN {
chomp (our $vchkpwBin ='/home/vpopmail/bin/vchkpw');
chomp (our $vpasswdBin ='/home/vpopmail/bin/vpasswd');
our $defaultDomain ='';
# The mysql configuration file used by vpopmail;
# we expect to use the same data
my $mysqlConfigFile ='/home/vpopmail/etc/vpopmail.mysql';
my $defaultDomainFile ='/home/vpopmail/etc/defaultdomain';
my $dictFile ='/usr/share/dict/words';
# Find hostname...
chomp (our $hostname = `hostname -f` || 'localhost.localdomain');
# Populate dictionary hash for rudimentary bad pw checking...
if (-f $dictFile && -r _) {
open (DICTFILE, $dictFile) or quitError("Can't open dictionary file: $!");
chomp, $words{$_}++ while ();
close DICTFILE;
};
if (-f $defaultDomainFile && -r _) {
open (DEFDOMAIN, $defaultDomainFile) or quitError("Can't determine default domain! $!");
chomp ($defaultDomain=);
close DEFDOMAIN;
$defaultDomain =~ s/^/@/ if $defaultDomain;
};
my ($server, $port, $dbuser, $dbpass, $db);
open (CONFIGFILE, $mysqlConfigFile) or quitError "Can't open mysql config file: $!";
while () {
next if /^#/ || /^$/;
chomp;
($server, $port, $dbuser, $dbpass, $db) = split(/\|/, $_, 5);
};
close CONFIGFILE;
if ($server =~ m/^localhost/) {
# The typical case, running MySQL locally
$dbh = DBI->connect("DBI:mysql:database=$db;mysql_client_found_rows=0",
$dbuser, $dbpass, { RaiseError => 1 });
} else {
$dbh = DBI->connect(join('',
'DBI:mysql:database=', $db,
';host=', $server,
';port=', $port,
';mysql_client_found_rows=0'), $dbuser, $dbpass, { RaiseError => 1 });
};
$dbh->{'mysql_auto_reconnect'} = 1;
$checkPassQuery=$dbh->prepare('SELECT pw_clear_passwd FROM vpopmail WHERE pw_name=? AND pw_domain=? AND pw_clear_passwd=?');
$updatePassQuery=$dbh->prepare('UPDATE vpopmail SET pw_clear_passwd=? WHERE pw_name=? AND pw_domain=? LIMIT 1');
$findIPQuery = $dbh->prepare('SELECT * FROM lastauth WHERE remote_ip=? LIMIT 1');
$logSuccessQuery=$dbh->prepare('INSERT INTO vlog SET (id, user, passwd, domain, logon, remoteip, message, timstamp, error) VALUES (NULL, ?, ?, ?, ?, ?, ?, NULL, ?)');
# Set strings...
# OK strings
$heloString = "200 $hostname vchkpassd; Who are you?$CRLF";
$oldPasswordString = "300 Thanks; your old password please?$CRLF";
$newPasswordString = "200 Nice to have you back. Enter your new password$CRLF";
$pwChangedString = "200 Password changed.$CRLF";
$noHelpString = "200 Help not available.$CRLF";
# Fail strings
$noUserGivenString = "100 I need more...$CRLF";
$badNewPassString = "500 Password change failed.$CRLF";
$unknownString = "500 Unknown command.$CRLF";
$needUsernameString = "500 Please enter your username.$CRLF";
$tooLateString = "500 Too late for that.$CRLF";
$mustQuitNowString = "500 Time to say goodbye.$CRLF";
# Quit strings
$badAuthString = "Incorrect username and/or password.";
$noDomainFoundString = "Can't determine proper domain. Try logging in as user\@example.com";
$tooManyTriesString = "Too many tries.";
$noRemoteIPString = "Can't determine where you're coming from.";
$remoteIPNotAuthString = "You must log in from this IP before you can change your password from it.";
};
##########################################################################
# Begin per-run code here
##########################################################################
%thisConn=();
my $badNewPW=0;
#quitError($noRemoteIPString) unless $ENV{REMOTE_HOST};
#do {
# $findIPQuery->execute($ENV{REMOTE_HOST});
# quitError($remoteIPNotAuthString) unless $findIPQuery->rows();
# } unless $ENV{REMOTE_HOST} =~ m/^127\./;
print $heloString;
while () {
s/$CR?$LF/\n/; chomp;
if (m/^USER/i) {
s/^USER\s?//i;
if ($_) {
$thisConn{user} = $_;
last;
} else {
print $noUserGivenString;
};
} elsif (m/^QUIT/i) {
quitNice();
} elsif (m/^HELP/i) {
print $noHelpString;
} else {
print $needUsernameString;
};
};
print $oldPasswordString;
while () {
s/$CR?$LF/\n/; chomp;
if (m/^PASS/i) {
s/^PASS\s?//i;
$thisConn{pass} = $_;
last;
} elsif (m/^QUIT/i) {
quitNice();
} elsif (m/^HELP/i) {
print $noHelpString;
} elsif (m/^USER/i) {
print $tooLateString;
} else {
print $unknownString;
};
};
quitError($badAuthString) unless ($thisConn{user} && $thisConn{pass});
# Determine our domain...
$thisConn{user} .= $defaultDomain unless $thisConn{user} =~ m/@/;
($thisConn{user}, $thisConn{domain}) = split (m/@/, $thisConn{user}, 2);
quitError($noDomainFoundString) unless $thisConn{domain};
#print "100 User is $thisConn{user}$CRLF", "100 Domain is $thisConn{domain}$CRLF", "100 Password is $thisConn{pass}$CRLF";
$checkPassQuery->execute($thisConn{user}, $thisConn{domain}, $thisConn{pass});
unless ($checkPassQuery->rows() == 1) {
sleep 3;
quitError($badAuthString);
};
print $newPasswordString;
while () {
s/$CR?$LF/\n/; chomp;
if (m/^NEWPASS/i) {
s/^NEWPASS\s?//i;
print ("500 Password cannot be empty.$CRLF"), next if ($_ eq '');
my $testpass = lc $_;
# Check for invalid new passwords...
print ("500 Sorry, spaces are not allowed (probably won't work anyway).$CRLF"), next if (m/\s/);
#print ("500 This password is WAY too short.$CRLF"), next unless (m/^\w\w\w/);
#print ("500 This password is too short.$CRLF"), next unless (m/^\w\w\w\w\w/);
#print ("500 Too simple; don't use all digits.$CRLF"), next if (m/^\d+$/);
#print ("500 Too simple; don't use something from the dictionary.$CRLF"), next if ($words{$testpass});
#print ("500 Too simple; don't use just lower-case letters.$CRLF"), next if (m/^[a-z]+$/);
#print ("500 Too simple; don't use just upper-case letters.$CRLF"), next if (m/^[A-Z]+$/);
#print ("500 Too simple; dictionary-word + single-digit is easily guessed.$CRLF"), next if ($testpass =~ m/^([a-z]+)\d$/ && $words{$1});
#print ("500 Too simple; single-digit + dictionary-word is easily guessed.$CRLF"), next if ($testpass =~ m/^\d([a-z]+)$/ && $words{$1});
$thisConn{newpass} = $_;
last;
} elsif (m/^QUIT/i) {
&quitNice();
} elsif (m/^HELP/i) {
print $noHelpString;
} elsif (m/^USER/i | m/^PASS/i) {
print $tooLateString;
} else {
print $unknownString;
};
} continue {
quitError($tooManyTriesString) if ($badNewPW++ > 3);
};
quitError("Password cannot be empty.") unless $thisConn{newpass};
system $vpasswdBin ($vpasswdBin, "$thisConn{user}\@$thisConn{domain}", $thisConn{newpass});
my $errorCode = $? >> 8;
if ($errorCode) {
warn "Error $errorCode attempting to change password for $thisConn{user}\@$thisConn{domain}: $@";
quitError("Password not changed. Error $errorCode");
} else {
print $pwChangedString;
};
while () {
s/$CR?$LF/\n/; chomp;
if (m/^QUIT/i) {
&quitNice();
} else {
print $mustQuitNowString;
};
};
# This program functions as a poppassd (port 106) daemon for use with
# MySQL-based qmail/vpopmail setups. It reads directly out of the
# vpopmail table in MySQL to verify passwords, but writes back using
# vpasswd.
# It includes some basic security features, including:
# - tarpitting (3 second delays on failure)
# - IP address stopping (connection must be from something in
# the lastauth table to be accepted)
# - uses vpasswd command to ensure encrypted pw placed in
#
# v 1.0 - Japheth Cleaver
# cleaver@rohan.sdsu.edu
#
# No caching for us.
$|=1;
use DBI;
use Socket qw(:DEFAULT :crlf);
local $/=$LF;
##########################################################################
sub quitNice {
print '200 Ja ne!', $CRLF;
close STDOUT;
exit;
};
sub quitError {
my $error = shift || 'An unknown error occurred';
print '500 ', $error, $CRLF;
close STDOUT;
exit;
};
##########################################################################
BEGIN {
chomp (our $vchkpwBin ='/home/vpopmail/bin/vchkpw');
chomp (our $vpasswdBin ='/home/vpopmail/bin/vpasswd');
our $defaultDomain ='';
# The mysql configuration file used by vpopmail;
# we expect to use the same data
my $mysqlConfigFile ='/home/vpopmail/etc/vpopmail.mysql';
my $defaultDomainFile ='/home/vpopmail/etc/defaultdomain';
my $dictFile ='/usr/share/dict/words';
# Find hostname...
chomp (our $hostname = `hostname -f` || 'localhost.localdomain');
# Populate dictionary hash for rudimentary bad pw checking...
if (-f $dictFile && -r _) {
open (DICTFILE, $dictFile) or quitError("Can't open dictionary file: $!");
chomp, $words{$_}++ while (
close DICTFILE;
};
if (-f $defaultDomainFile && -r _) {
open (DEFDOMAIN, $defaultDomainFile) or quitError("Can't determine default domain! $!");
chomp ($defaultDomain=
close DEFDOMAIN;
$defaultDomain =~ s/^/@/ if $defaultDomain;
};
my ($server, $port, $dbuser, $dbpass, $db);
open (CONFIGFILE, $mysqlConfigFile) or quitError "Can't open mysql config file: $!";
while (
next if /^#/ || /^$/;
chomp;
($server, $port, $dbuser, $dbpass, $db) = split(/\|/, $_, 5);
};
close CONFIGFILE;
if ($server =~ m/^localhost/) {
# The typical case, running MySQL locally
$dbh = DBI->connect("DBI:mysql:database=$db;mysql_client_found_rows=0",
$dbuser, $dbpass, { RaiseError => 1 });
} else {
$dbh = DBI->connect(join('',
'DBI:mysql:database=', $db,
';host=', $server,
';port=', $port,
';mysql_client_found_rows=0'), $dbuser, $dbpass, { RaiseError => 1 });
};
$dbh->{'mysql_auto_reconnect'} = 1;
$checkPassQuery=$dbh->prepare('SELECT pw_clear_passwd FROM vpopmail WHERE pw_name=? AND pw_domain=? AND pw_clear_passwd=?');
$updatePassQuery=$dbh->prepare('UPDATE vpopmail SET pw_clear_passwd=? WHERE pw_name=? AND pw_domain=? LIMIT 1');
$findIPQuery = $dbh->prepare('SELECT * FROM lastauth WHERE remote_ip=? LIMIT 1');
$logSuccessQuery=$dbh->prepare('INSERT INTO vlog SET (id, user, passwd, domain, logon, remoteip, message, timstamp, error) VALUES (NULL, ?, ?, ?, ?, ?, ?, NULL, ?)');
# Set strings...
# OK strings
$heloString = "200 $hostname vchkpassd; Who are you?$CRLF";
$oldPasswordString = "300 Thanks; your old password please?$CRLF";
$newPasswordString = "200 Nice to have you back. Enter your new password$CRLF";
$pwChangedString = "200 Password changed.$CRLF";
$noHelpString = "200 Help not available.$CRLF";
# Fail strings
$noUserGivenString = "100 I need more...$CRLF";
$badNewPassString = "500 Password change failed.$CRLF";
$unknownString = "500 Unknown command.$CRLF";
$needUsernameString = "500 Please enter your username.$CRLF";
$tooLateString = "500 Too late for that.$CRLF";
$mustQuitNowString = "500 Time to say goodbye.$CRLF";
# Quit strings
$badAuthString = "Incorrect username and/or password.";
$noDomainFoundString = "Can't determine proper domain. Try logging in as user\@example.com";
$tooManyTriesString = "Too many tries.";
$noRemoteIPString = "Can't determine where you're coming from.";
$remoteIPNotAuthString = "You must log in from this IP before you can change your password from it.";
};
##########################################################################
# Begin per-run code here
##########################################################################
%thisConn=();
my $badNewPW=0;
#quitError($noRemoteIPString) unless $ENV{REMOTE_HOST};
#do {
# $findIPQuery->execute($ENV{REMOTE_HOST});
# quitError($remoteIPNotAuthString) unless $findIPQuery->rows();
# } unless $ENV{REMOTE_HOST} =~ m/^127\./;
print $heloString;
while (
s/$CR?$LF/\n/; chomp;
if (m/^USER/i) {
s/^USER\s?//i;
if ($_) {
$thisConn{user} = $_;
last;
} else {
print $noUserGivenString;
};
} elsif (m/^QUIT/i) {
quitNice();
} elsif (m/^HELP/i) {
print $noHelpString;
} else {
print $needUsernameString;
};
};
print $oldPasswordString;
while (
s/$CR?$LF/\n/; chomp;
if (m/^PASS/i) {
s/^PASS\s?//i;
$thisConn{pass} = $_;
last;
} elsif (m/^QUIT/i) {
quitNice();
} elsif (m/^HELP/i) {
print $noHelpString;
} elsif (m/^USER/i) {
print $tooLateString;
} else {
print $unknownString;
};
};
quitError($badAuthString) unless ($thisConn{user} && $thisConn{pass});
# Determine our domain...
$thisConn{user} .= $defaultDomain unless $thisConn{user} =~ m/@/;
($thisConn{user}, $thisConn{domain}) = split (m/@/, $thisConn{user}, 2);
quitError($noDomainFoundString) unless $thisConn{domain};
#print "100 User is $thisConn{user}$CRLF", "100 Domain is $thisConn{domain}$CRLF", "100 Password is $thisConn{pass}$CRLF";
$checkPassQuery->execute($thisConn{user}, $thisConn{domain}, $thisConn{pass});
unless ($checkPassQuery->rows() == 1) {
sleep 3;
quitError($badAuthString);
};
print $newPasswordString;
while (
s/$CR?$LF/\n/; chomp;
if (m/^NEWPASS/i) {
s/^NEWPASS\s?//i;
print ("500 Password cannot be empty.$CRLF"), next if ($_ eq '');
my $testpass = lc $_;
# Check for invalid new passwords...
print ("500 Sorry, spaces are not allowed (probably won't work anyway).$CRLF"), next if (m/\s/);
#print ("500 This password is WAY too short.$CRLF"), next unless (m/^\w\w\w/);
#print ("500 This password is too short.$CRLF"), next unless (m/^\w\w\w\w\w/);
#print ("500 Too simple; don't use all digits.$CRLF"), next if (m/^\d+$/);
#print ("500 Too simple; don't use something from the dictionary.$CRLF"), next if ($words{$testpass});
#print ("500 Too simple; don't use just lower-case letters.$CRLF"), next if (m/^[a-z]+$/);
#print ("500 Too simple; don't use just upper-case letters.$CRLF"), next if (m/^[A-Z]+$/);
#print ("500 Too simple; dictionary-word + single-digit is easily guessed.$CRLF"), next if ($testpass =~ m/^([a-z]+)\d$/ && $words{$1});
#print ("500 Too simple; single-digit + dictionary-word is easily guessed.$CRLF"), next if ($testpass =~ m/^\d([a-z]+)$/ && $words{$1});
$thisConn{newpass} = $_;
last;
} elsif (m/^QUIT/i) {
&quitNice();
} elsif (m/^HELP/i) {
print $noHelpString;
} elsif (m/^USER/i | m/^PASS/i) {
print $tooLateString;
} else {
print $unknownString;
};
} continue {
quitError($tooManyTriesString) if ($badNewPW++ > 3);
};
quitError("Password cannot be empty.") unless $thisConn{newpass};
system $vpasswdBin ($vpasswdBin, "$thisConn{user}\@$thisConn{domain}", $thisConn{newpass});
my $errorCode = $? >> 8;
if ($errorCode) {
warn "Error $errorCode attempting to change password for $thisConn{user}\@$thisConn{domain}: $@";
quitError("Password not changed. Error $errorCode");
} else {
print $pwChangedString;
};
while (
s/$CR?$LF/\n/; chomp;
if (m/^QUIT/i) {
&quitNice();
} else {
print $mustQuitNowString;
};
};
2. chmod 755 /usr/bin/vchkpassd
3. aptitude install openbsd-inetd
4. nano /etc/inetd.conf
Add the following under "#:MAIL: Mail, news and uucp services.":
3. aptitude install openbsd-inetd
4. nano /etc/inetd.conf
Add the following under "#:MAIL: Mail, news and uucp services.":
poppassd stream tcp nowait root /usr/sbin/vchkpassd
5. /etc/init.d/openbsd-inetd restart
To test, try the following:
To test, try the following:
telnet localhost 106
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
200 cloud6 vchkpassd; Who are you?
user someone@somewhere.com
300 Thanks; your old password please?
pass oldpassword
200 Nice to have you back. Enter your new password
newpass newpassword
200 Password changed.
quit
200 Ja ne!
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
200 cloud6 vchkpassd; Who are you?
user someone@somewhere.com
300 Thanks; your old password please?
pass oldpassword
200 Nice to have you back. Enter your new password
newpass newpassword
200 Password changed.
quit
200 Ja ne!
Comments
Post a Comment