diff --git a/lib/Qpsmtpd.pm b/lib/Qpsmtpd.pm index 025a761..dc01b48 100644 --- a/lib/Qpsmtpd.pm +++ b/lib/Qpsmtpd.pm @@ -487,20 +487,30 @@ sub size_threshold { return $Size_threshold; } +sub authenticated { + my ($self, $state) = @_; + $self->{_auth_state} = $state if $state; + return (defined $self->{_auth_state} ? $self->{_auth_state} : 0); +} + sub auth_user { my ($self, $user) = @_; - $user =~ s/[\r\n].*//s; - $self->{_auth_user} = $user if $user; + $self->{_auth_user} = $user if $user; return (defined $self->{_auth_user} ? $self->{_auth_user} : "" ); } +sub auth_ticket { + my ($self, $ticket) = @_; + $self->{_auth_ticket} = $ticket if $ticket; + return (defined $self->{_auth_ticket} ? $self->{_auth_ticket} : "" ); +} + sub auth_mechanism { my ($self, $mechanism) = @_; - $mechanism =~ s/[\r\n].*//s; - $self->{_auth_mechanism} = $mechanism if $mechanism; + $self->{_auth_mechanism} = lc($mechanism) if $mechanism; return (defined $self->{_auth_mechanism} ? $self->{_auth_mechanism} : "" ); } - + sub fd { return shift->{fd}; } diff --git a/lib/Qpsmtpd/Auth.pm b/lib/Qpsmtpd/Auth.pm index ada6173..e5ed01a 100644 --- a/lib/Qpsmtpd/Auth.pm +++ b/lib/Qpsmtpd/Auth.pm @@ -214,119 +214,4 @@ Please see the LICENSE file included with qpsmtpd for details. =cut -package Qpsmtpd::Auth; -use Qpsmtpd::Constants; -use MIME::Base64; - -sub e64 -{ - my ($arg) = @_; - my $res = encode_base64($arg); - chomp($res); - return($res); -} - -sub SASL { - - # $DB::single = 1; - my ( $session, $mechanism, $prekey ) = @_; - my ( $user, $passClear, $passHash, $ticket ); - $mechanism = lc($mechanism); - - if ( $mechanism eq "plain" ) { - if (!$prekey) { - $session->respond( 334, "Please continue" ); - $prekey= <>; - } - ( $passHash, $user, $passClear ) = split /\x0/, - decode_base64($prekey); - - } - elsif ($mechanism eq "login") { - - if ( $prekey ) { - ($passHash, $user, $passClear) = split /\x0/, decode_base64($prekey); - } - else { - - $session->respond(334, e64("Username:")); - $user = decode_base64(<>); - #warn("Debug: User: '$user'"); - if ($user eq '*') { - $session->respond(501, "Authentification canceled"); - return DECLINED; - } - - $session->respond(334, e64("Password:")); - $passClear = <>; - $passClear = decode_base64($passClear); - #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); - - if ( $line eq '*' ) { - $session->respond( 501, "Authentification canceled" ); - return DECLINED; - } - - ( $user, $passHash ) = split( ' ', decode_base64($line) ); - - } - else { - $session->respond( 500, "Unrecognized authentification mechanism" ); - return DECLINED; - } - - # try running the specific hooks first - my ( $rc, $msg ) = - $session->run_hooks( "auth-$mechanism", $mechanism, $user, $passClear, - $passHash, $ticket ); - - # try running the polymorphous hooks next - if ( !$rc || $rc == DECLINED ) { - ( $rc, $msg ) = - $session->run_hooks( "auth", $mechanism, $user, $passClear, - $passHash, $ticket ); - } - - if ( $rc == OK ) { - $msg = "Authentication successful for $user" . - ( defined $msg ? " - " . $msg : "" ); - $session->respond( 235, $msg ); - $session->connection->relay_client(1); - $session->log( LOGINFO, $msg ); - - $session->auth_user($user); - $session->auth_mechanism($mechanism); - - return OK; - } - else { - $msg = "Authentication failed for $user" . - ( defined $msg ? " - " . $msg : "" ); - $session->respond( 535, $msg ); - $session->log( LOGERROR, $msg ); - return DENY; - } -} - -# tag: qpsmtpd plugin that sets RELAYCLIENT when the user authentifies - 1; diff --git a/lib/Qpsmtpd/Constants.pm b/lib/Qpsmtpd/Constants.pm index 8be3268..27bebf0 100644 --- a/lib/Qpsmtpd/Constants.pm +++ b/lib/Qpsmtpd/Constants.pm @@ -26,6 +26,7 @@ my %return_codes = ( DECLINED => 909, DONE => 910, CONTINUATION => 911, + AUTH_PENDING => 912, ); use vars qw(@ISA @EXPORT); diff --git a/lib/Qpsmtpd/PollServer.pm b/lib/Qpsmtpd/PollServer.pm index c8a1b17..a6db0d4 100644 --- a/lib/Qpsmtpd/PollServer.pm +++ b/lib/Qpsmtpd/PollServer.pm @@ -15,9 +15,10 @@ use fields qw( hooks start_time cmd_timeout - _auth - _auth_user _auth_mechanism + _auth_state + _auth_ticket + _auth_user _commands _config_cache _connection @@ -158,6 +159,9 @@ sub process_cmd { } return $resp; } + elsif ( $self->authenticated == AUTH_PENDING ) { + return $self->auth_process($line); + } else { # No such method - i.e. unrecognized command my ($rc, $msg) = $self->run_hooks("unrecognized_command", $meth, @params); @@ -315,7 +319,7 @@ sub end_of_data { } # only true if client authenticated - if ( defined $self->{_auth} and $self->{_auth} == OK ) { + if ( $self->authenticated == OK ) { $header->add("X-Qpsmtpd-Auth","True"); } diff --git a/lib/Qpsmtpd/SMTP.pm b/lib/Qpsmtpd/SMTP.pm index a61d4e7..87f0118 100644 --- a/lib/Qpsmtpd/SMTP.pm +++ b/lib/Qpsmtpd/SMTP.pm @@ -12,6 +12,7 @@ use Qpsmtpd::Auth; use Qpsmtpd::Address (); use Mail::Header (); +use MIME::Base64; #use Data::Dumper; use POSIX qw(strftime); use Net::DNS; @@ -48,6 +49,11 @@ sub dispatch { $self->{_counter}++; + if ( $self->authenticated == AUTH_PENDING ) { + # must be in the middle of prompting for auth parameters + return $self->auth_process($cmd,@_); + } + if ($cmd !~ /^(\w{1,12})$/ or !exists $self->{_commands}->{$1}) { my ($rc, $msg) = $self->run_hooks("unrecognized_command", $cmd, @_); return $self->unrecognized_command_respond($rc, $msg, @_) unless $rc == CONTINUATION; @@ -114,13 +120,13 @@ sub connect_respond { elsif ($rc != DONE) { my $greets = $self->config('smtpgreeting'); if ( $greets ) { - $greets .= " ESMTP"; + $greets .= " ESMTP"; } else { - $greets = $self->config('me') - . " ESMTP qpsmtpd " - . $self->version - . " ready; send us your mail, but not your spam."; + $greets = $self->config('me') + . " ESMTP qpsmtpd " + . $self->version + . " ready; send us your mail, but not your spam."; } $self->respond(220, $greets); @@ -197,8 +203,8 @@ sub ehlo_respond { $self->transaction; my @capabilities = $self->transaction->notes('capabilities') - ? @{ $self->transaction->notes('capabilities') } - : (); + ? @{ $self->transaction->notes('capabilities') } + : (); # Check for possible AUTH mechanisms my %auth_mechanisms; @@ -229,17 +235,148 @@ HOOK: foreach my $hook ( keys %{$self->{hooks}} ) { } } +sub e64 +{ + my ($arg) = @_; + my $res = encode_base64($arg); + chomp($res); + return($res); +} + sub auth { - my ( $self, $arg, @stuff ) = @_; + my ( $self, $mechanism, $prekey ) = @_; #they AUTH'd once already return $self->respond( 503, "but you already said AUTH ..." ) - if ( defined $self->{_auth} - and $self->{_auth} == OK ); + if ( $self->authenticated == OK ); return $self->respond( 503, "AUTH not defined for HELO" ) if ( $self->connection->hello eq "helo" ); - return $self->{_auth} = Qpsmtpd::Auth::SASL( $self, $arg, @stuff ); + # $DB::single = 1; + + $self->auth_mechanism($mechanism); + $self->authenticated(AUTH_PENDING); + if ( $prekey ) { # easy single step + unless ( $mechanism =~ /^(plain|login)$/i ) { + # must be plain or login + $self->respond( 500, "Unrecognized authentification mechanism" ); + return DECLINED; + } + my ($passHash, $user, $passClear) = split /\x0/,decode_base64($prekey); + # we have all of the elements ready to go now + if ( $mechanism =~ /login/i ) { + $self->auth_user($user); + return $self->auth_process(e64($passClear)); + } + else { + return $self->auth_process($prekey); + } + } + else { + if ( $mechanism =~ /plain/i ) { + $self->respond( 334, "Please continue" ); + } + elsif ( $mechanism =~ /login/i ) { + $self->respond( 334, e64("Username:") ); + } + elsif ( $mechanism =~ /cram-md5/i ) { + # 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, or if the clock is skewed. + my $ticket = sprintf( "<%x.%x\@" . $self->config("me") . ">", + rand(1000000), time() ); + + # Store this for later + $self->auth_ticket($ticket); + # We send the ticket encoded in Base64 + $self->respond( 334, encode_base64( $ticket, "" ) ); + } + } + return DECLINED; +} + +sub auth_process { + my ($self, $line) = @_; + my ( $user, $passClear, $passHash, $ticket, $mechanism ); + + # do this once here + $mechanism = $self->auth_mechanism; + $user = $self->auth_user; + $ticket = $self->auth_ticket; + + if ( $mechanism eq 'plain' ) { + ( $passHash, $user, $passClear ) = split /\x0/, + decode_base64($line); + } + elsif ( $mechanism eq 'login' ) { + if ( $user ) { + # must be getting the password now + $passClear = decode_base64($line); + } + else { + # must be getting the user now + $user = decode_base64($line); + $self->auth_user($user); + $self->respond(334, e64("Password:")); + } + } + elsif ( $mechanism eq "cram-md5" ) { + $line =~ tr/[\r\n]//d; # cannot simply chomp CRLF + + ( $user, $passHash ) = split( ' ', decode_base64($line) ); + + } + else { + $self->respond( 500, "Unrecognized authentification mechanism" ); + return DECLINED; + } + if ($user eq '*') { + $self->respond(501, "Authentification canceled"); + return DECLINED; + } + + # check to see if we can proceed with the hooks + if ( $user and ( $passClear or $passHash ) ) { + # try running the specific hooks first + my ( $rc, $msg ) = + $self->run_hooks( "auth-$mechanism", + $mechanism, $user, $passClear, + $passHash, $ticket ); + + # try running the polymorphous hooks next + if ( !$rc || $rc == DECLINED ) { + ( $rc, $msg ) = + $self->run_hooks( "auth", $mechanism, $user, $passClear, + $passHash, $ticket ); + } + return $self->auth_respond($rc, $msg, $mechanism, $user) + unless $rc == CONTINUATION; + } + else { + return CONTINUATION; + } +} + + +sub auth_respond { + my ($self, $rc, $msg, $mechanism, $user) = @_; + if ( $rc == OK ) { + $msg = "Authentication successful for $user" . + ( defined $msg ? " - " . $msg : "" ); + $self->respond( 235, $msg ); + $self->connection->relay_client(1); + $self->log( LOGINFO, $msg ); + $self->authenticated(OK); + + return OK; + } + else { + $msg = "Authentication failed for $user" . + ( defined $msg ? " - " . $msg : "" ); + $self->respond( 535, $msg ); + $self->log( LOGERROR, $msg ); + return DENY; + } } sub mail { @@ -541,8 +678,8 @@ sub data_respond { # FIXME - call plugins to work on just the header here; can # save us buffering the mail content. - # Save the start of just the body itself - $self->transaction->set_body_start(); + # Save the start of just the body itself + $self->transaction->set_body_start(); } @@ -564,8 +701,9 @@ sub data_respond { $self->transaction->header($header); my $smtp = $self->connection->hello eq "ehlo" ? "ESMTP" : "SMTP"; - my $authheader = (defined $self->{_auth} and $self->{_auth} == OK) ? - "(smtp-auth username $self->{_auth_user}, mechanism $self->{_auth_mechanism})\n" : ""; + my $authheader = ($self->authenticated == OK) + ? "(smtp-auth username $self->auth_user, mechanism $self->auth_mechanism)\n" + : ""; $header->add("Received", "from ".$self->connection->remote_info ." (HELO ".$self->connection->hello_host . ") (".$self->connection->remote_ip