A few unordered comments:
1) The two sample plugins are completely lame (one always fails and one always authenticates); actual backends that do something are left as an exercise for the user (I'll do vpopmail tomorrow probably).
2) Due to the way that CRAM-MD5 is defined, any backend using that method must have access to the plaintext password (so that it can be compared to the MD5-ified version sent by the client).
3) I haven't decided where to put logging statements in the code, nor have I decided how to document this mess.
4) The {_commands} hash is not actually used to limit what commands are available (which is why auth() is located in Qpsmtpd::Auth). I don't think this is very helpful (it meant that initially HELO would support AUTH, which it shouldn't). My somewhat abusive hack to just stuff the method into the parent namespace works, but violates the spirit of OO code, methinks.
5) I've set things up to handle two completely different type of auth plugins:
a) A polymorphous plugin which is required to support all three authentication methods;
b) A specified plugin which can choose to support any number (or all three) mechanisms;
The more general plugs are attempted first. I haven't actually gotten the code to work such that multiple plugins can be registered on the same hook and all will be tried before finally failing completely. I think I am just missing the correct return code for that case.
6) According to at least one site I googled, very few mail clients support DIGEST-MD5 (or SCRAM-MD5 for that matter), and it would be much more complex to support those methods, so I have simply blown those methods off. You got the itch, _you_ scratch it! ;)
Anyway, share and enjoy!
John
=== plugins/authdeny
==================================================================
--- plugins/authdeny (/cvs/trunk) (revision 428)
+++ plugins/authdeny (/local/trunk) (revision 428)
@@ -0,0 +1,19 @@
+#!/usr/bin/perl
+#
+# This plugin doesn't actually check anything and will fail any
+# user no matter what they type. It is strictly a proof of concept for
+# the Qpsmtpd::Auth module. Don't run this in production!!!
+#
+
+sub register {
+ my ($self, $qp) = @_;
+ $self->register_hook("auth", "authdeny");
+}
+
+sub authdeny {
+ my ($self,$transaction,$method,$user,$passClear,$passHash,$ticket) = @_;
+# $DB::single = 1;
+
+ return (DENY, "$user is not free to abuse my relay");
+}
+
=== plugins/authnull
==================================================================
--- plugins/authnull (/cvs/trunk) (revision 428)
+++ plugins/authnull (/local/trunk) (revision 428)
@@ -0,0 +1,25 @@
+#!/usr/bin/perl
+#
+# This plugin doesn't actually check anything and will authenticate any
+# user no matter what they type. It is strictly a proof of concept for
+# the Qpsmtpd::Auth module. Don't run this in production!!!
+#
+
+sub register {
+ my ($self, $qp) = @_;
+
+ $self->register_hook("auth-plain", "authnull");
+# $self->register_hook("auth-login", "authnull");
+# $self->register_hook("auth-cram-md5", "authnull");
+
+# $self->register_hook("auth", "authnull");
+}
+
+sub authnull {
+ my ($self, $transaction, $method, $user, $passClear, $passHash, $ticket) = @_;
+# $DB::single = 1;
+ $self->log(LOGERROR,"authenticating $user using $method");
+
+ return (OK, "$user is free to abuse my relay");
+}
+
=== lib/Qpsmtpd/Plugin.pm
==================================================================
--- lib/Qpsmtpd/Plugin.pm (/cvs/trunk) (revision 428)
+++ lib/Qpsmtpd/Plugin.pm (/local/trunk) (revision 428)
@@ -3,6 +3,7 @@
my %hooks = map { $_ => 1 } qw(
config queue data_post quit rcpt mail ehlo helo
+ auth auth-plain auth-login auth-cram-md5
connect reset_transaction unrecognized_command disconnect
);
=== lib/Qpsmtpd/Auth.pm
==================================================================
--- lib/Qpsmtpd/Auth.pm (/cvs/trunk) (revision 428)
+++ lib/Qpsmtpd/Auth.pm (/local/trunk) (revision 428)
@@ -0,0 +1,112 @@
+#!/usr/bin/perl -d
+package Qpsmtpd::Auth;
+use Qpsmtpd::Constants;
+use MIME::Base64;
+
+sub auth {
+ my ($self, $arg, @stuff) = @_;
+ #they AUTH'd once already
+ return $self->respond (503, "but you already said AUTH ...")
+ if ( $self->{_auth} == OK );
+
+ return $self->{_auth} = Qpsmtpd::Auth::SASL($self,$arg);
+}
+
+sub SASL {
+ my ($session, $mechanism, @stuff) = @_;
+ my ($user, $passClear, $passHash, $ticket);
+ $mechanism = lc($mechanism);
+
+ if ($mechanism eq "plain") {
+ $session->respond(334, "Username:");
+
+ # We read the username and password from STDIN
+ $user = <>;
+ chop($user); chop($user);
+ #warn("Debug: User: '$user'");
+ if ($user eq '*') {
+ $session->respond(501, "Authentification canceled");
+ return DECLINED;
+ }
+
+ $session->respond(334, "Password:");
+ $passClear = <>;
+ chop($passClear); chop($passClear);
+ #warn("Debug: Pass: '$pass'");
+ if ($passClear eq '*') {
+ $session->respond(501, "Authentification canceled");
+ return DECLINED;
+ }
+
+ }
+ elsif ($mechanism eq "login") {
+ $session->respond(334, encode_base64("Username:"));
+ $user = decode_base64(<>);
+ #warn("Debug: User: '$user'");
+ if ($user eq '*') {
+ $session->respond(501, "Authentification canceled");
+ return DECLINED;
+ }
+
+ $session->respond(334, encode_base64("Password:"));
+ $passClear = decode_base64(<>);
+ #warn("Debug: Pass: '$pass'");
+ if ($passClear eq '*') {
+ $session->respond(501, "Authentification canceled");
+ return DECLINED;
+ }
+
+ }
+ elsif ($mechanism eq "cram-md5") {
+ # rand() is not cryptographic, but we only need to generate a globally
+ # unique number. The rand() is there in case the user logs in more than
+ # once in the same second, of if the clock is skewed.
+ $ticket = sprintf("<%x.%x\@".$session->config("me").">",
+ rand(1000000), time());
+
+ # We send the ticket encoded in Base64
+ $session->respond(334, encode_base64($ticket,""));
+ my $line = <>;
+ chop($line); chop($line);
+ #warn("Debug: Received: '$line', decodes to: '" . decode_base64($line) . "'");
+ if ($line eq '*') {
+ $session->respond(501, "Authentification canceled");
+ return DECLINED;
+ }
+
+ ($user, $passHash) = split(' ', decode_base64($line));
+ #warn("Debug: User: '$user', passhash='$passHash'");
+
+ }
+ else {
+ $session->respond(500, "Unrecognized authentification mechanism");
+ return DECLINED;
+ }
+
+ # try running the polymorphous hooks first
+ my ($rc, $msg) = $session->run_hooks("auth",
+ $mechanism, $user, $passClear, $passHash, $ticket);
+ $DB::single = 1;
+
+ unless ($rc == OK ) { # try running the specified hooks next
+ ($rc, $msg) = $session->run_hooks("auth-$mechanism",
+ $mechanism, $user, $passClear, $passHash, $ticket);
+ }
+
+ if ( $rc == OK ) {
+ $session->respond(235, "Authentication successful - $msg");
+ $ENV{RELAYCLIENT} = 1;
+ return OK;
+ }
+ else {
+ $msg = "Authentication failed" unless $msg;
+ $session->respond(535, $msg);
+ $session->log(LOGERROR, $msg);
+ return DENY;
+ }
+}
+
+# tag: qpsmtpd plugin that sets RELAYCLIENT when the user authentifies
+
+
+1;
=== lib/Qpsmtpd/SMTP.pm
==================================================================
--- lib/Qpsmtpd/SMTP.pm (/cvs/trunk) (revision 428)
+++ lib/Qpsmtpd/SMTP.pm (/local/trunk) (revision 428)
@@ -10,6 +10,7 @@
use Qpsmtpd::Transaction;
use Qpsmtpd::Plugin;
use Qpsmtpd::Constants;
+use Qpsmtpd::Auth;
use Mail::Address ();
use Mail::Header ();
@@ -124,7 +125,6 @@
return $self->{_connection} || ($self->{_connection} = Qpsmtpd::Connection->new());
}
-
sub helo {
my ($self, $hello_host, @stuff) = @_;
my $conn = $self->connection;
@@ -165,6 +165,26 @@
my @capabilities = $self->transaction->notes('capabilities')
? @{ $self->transaction->notes('capabilities') }
: ();
+
+ # Check for possible AUTH mechanisms
+ my %auth_mechanisms;
+HOOK: foreach my $hook ( keys %{$self->{_hooks}} ) {
+ if ( $hook =~ m/^auth-?(.+)?$/ ) {
+ if ( defined $1 ) {
+ $auth_mechanisms{uc($1)} = 1;
+ }
+ else { # at least one polymorphous auth provider
+ %auth_mechanisms = map {$_,1} qw(PLAIN LOGIN CRAM-MD5);
+ last HOOK;
+ }
+ }
+ }
+
+ if ( %auth_mechanisms ) {
+ push @capabilities, 'AUTH '.join(" ",keys(%auth_mechanisms));
+ $self->{_commands}->{'auth'} = "";
+ *auth = \&Qpsmtpd::Auth::auth;
+ }
$self->respond(250,
$self->config("me") . " Hi " . $conn->remote_info . " [" .
$conn->remote_ip ."]",
