kuvert/plainAUTH.pm

185 lines
4.0 KiB
Perl

package Net::Server::Mail::ESMTP::plainAUTH;
use strict;
use base qw(Net::Server::Mail::ESMTP::Extension);
use MIME::Base64;
use vars qw( $VERSION );
$VERSION = '1.0';
# the following are required by nsme::extension
# but not documented :(
sub init
{
my ($self,$parent)=@_;
$self->{AUTH}=();
return $self;
}
# the smtp operations we add
sub verb
{
return ( [ 'AUTH' => \&handle_auth, ],);
}
# what to add to the esmtp capabilities response
sub keyword
{
return 'AUTH LOGIN PLAIN';
}
# what options to allow for mail from: auth
sub option
{
return (['MAIL', 'AUTH' => sub { return; }]);
}
# and the actual auth handler
sub handle_auth
{
my ($self,$args)=@_;
my ($method,$param);
$args=~/^(LOGIN|PLAIN)\s*(.*)$/ && (($method,$param)=($1,$2));
if ($self->{AUTH}->{active})
{
delete $self->{AUTH}->{active};
$self->reply(535, "Authentication phases mixed up.");
return undef; # if rv given, server shuts conn!
}
elsif ($self->{AUTH}->{completed})
{
$self->reply(504,"Already authenticated.");
return undef;
}
elsif (!$method)
{
$self->reply(501,"Unknown authentication method.");
return undef;
}
$self->{AUTH}->{active}=$method;
if ($param eq '*')
{
delete $self->{AUTH}->{active};
$self->reply(501, "Authentication cancelled.");
return undef;
}
if ($method eq 'PLAIN')
{
if ($param) # plain: immediate with args
{
my (undef,$user,$pwd)=split(/\0/,decode_base64($param),3);
if (!$user)
{
delete $self->{AUTH}->{active};
$self->reply(535, "5.7.8 Authentication failed.");
return undef;
}
return run_callback($self,$user,$pwd);
}
else # plain: or empty challenge and then response
{
$self->reply(334," ");
# undocumented but crucial: direct stuff to this method
$self->next_input_to(\&process_response);
return undef;
}
}
elsif ($method eq 'LOGIN')
{
# login is always two challenges
$self->reply(334, "VXNlcm5hbWU6"); # username
$self->next_input_to(\&process_response);
return undef;
}
}
# runs user-supplied callback on username and password
# responds success if callback succeeds
# sets complete if ok, clears active either way
sub run_callback
{
my ($self,$user,$pass)=@_;
my $ok;
my $ref=$self->{callback}->{AUTH};
if (ref $ref eq 'ARRAY' && ref $ref->[0] eq 'CODE')
{
my $c=$ref->[0];
$ok=&$c($self,$user,$pass);
}
if ($ok)
{
$self->reply(235, "Authentication successful");
$self->{AUTH}->{completed}=1;
}
else
{
$self->reply(535,"Authentication failed.");
}
delete $self->{AUTH}->{active};
return undef;
}
# deals with any response, based on active method
sub process_response
{
my ($self,$args)=@_;
if (!$self->{AUTH}->{active} || $self->{AUTH}->{completed})
{
delete $self->{AUTH}->{active};
$self->reply(535, "Authentication phases mixed up.");
return undef;
}
if (!$args)
{
delete $self->{AUTH}->{active};
$self->reply(535, "5.7.8 Authentication failed.");
return undef;
}
if ($self->{AUTH}->{active} eq "PLAIN")
{
# plain is easy: only one response containing everything
my (undef,$user,$pwd)=split(/\0/,decode_base64($args),3);
if (!$user)
{
delete $self->{AUTH}->{active};
$self->reply(535, "5.7.8 Authentication failed.");
return undef;
}
return run_callback($self,$user,$pwd);
}
elsif ($self->{AUTH}->{active} eq "LOGIN")
{
# uglier: two challenges for username+password
my ($input)=split(/\0/,decode_base64($args));
# is this the second time round?
if ($self->{AUTH}->{user})
{
return run_callback($self,$self->{AUTH}->{user},$input);
}
else
{
# nope, first time: save username and challenge
# for password
$self->{AUTH}->{user}=$input;
$self->reply(334, "UGFzc3dvcmQ6"); # password
$self->next_input_to(\&process_response);
return undef;
}
}
else
{
delete $self->{AUTH}->{active};
$self->reply(535, "Authentication mixed up.");
return undef;
}
}
1;