Hello Steve,

On Wed, 3 Dec 2003 10:41 pm, Steve Rogers wrote:
> Hi,
>
> We are using TACACS with AuthLog FILE and logging the username from
> Access-Requests. Our config file excerpts are below. The problem is that
> using %u we never see the username logged from the User-Name attribute
> in the radius Access Request. Changing the %u to %{User-Name} works with
> no problems.

Thanks for reporting this.
We have issued a patch that should fix this problem, and attached a new 
ServerTACACSPLUS.pm for you to test.

We apologise for this problem.

Cheers.

>
> Config file:
>
> <AuthLog FILE>
>         Identifier      AuthLogger
>         Filename        %L/%Y%m%d-auth.log
>         SuccessFormat   %l:%c:%u:OK
>         FailureFormat   %l:%c:%u:FAIL
>         LogSuccess      1
>         LogFailure      1
> </AuthLog>
>
> <ServerTACACSPLUS>
>         Key
>         AddToRequest NAS-Identifier=TACACS
> </ServerTACACSPLUS>
>
> <Realm DEFAULT>
>         AuthLog AuthLogger
>         <AuthBy FILE>
>                 Filename tacacs.users
>         </AuthBy>
>         AcctLogFileName %L/%Y%m%d-acct.log
> </Realm>
>
> Authlog result:
>
> Tue Dec  2 16:05:25 2003:192.168.x.x.::OK
>
> After changing the AuthLog format to use %{User-Name} the logfile looks
> like:
>
> Wed Dec  3 10:06:21 2003:192.168.x.x:justin:OK
>
> Using radiator 3.7.1 with latest patches.
>
> Thanks
> Steve

-- 
Mike McCauley                               [EMAIL PROTECTED]
Open System Consultants Pty. Ltd            Unix, Perl, Motif, C++, WWW
24 Bateman St Hampton, VIC 3188 Australia   http://www.open.com.au
Phone +61 3 9598-0985                       Fax   +61 3 9598-0955

Radiator: the most portable, flexible and configurable RADIUS server 
anywhere. SQL, proxy, DBM, files, LDAP, NIS+, password, NT, Emerald, 
Platypus, Freeside, TACACS+, PAM, external, Active Directory, EAP, TLS, 
TTLS, PEAP etc on Unix, Windows, MacOS etc.
# ServerTACACSPLUS.pm
#
# Object for receiving TACACS+ requests and satisfying them
# Incoming TACACS+ authentication requests are converted into 
# Radius requests. ASCII, PAP, CHAP and MSCHAP are supported.
# Incoming TACACS+ authorization requests are always approved,
# and any cisco-avpair reply items from the previous Radius Access-Accept are 
# used as authorization attribute-value pairs
# Incoming TACACS+ accounting requests are converted into Radius
# accounting requests.
#
# Based on draft-grant-tacacs-02.txt 
#
# Author: Mike McCauley ([EMAIL PROTECTED])
# Copyright (C) 2003 Open System Consultants
# $Id: ServerTACACSPLUS.pm,v 1.15 2003/12/03 22:03:42 mikem Exp mikem $

package Radius::ServerTACACSPLUS;
@ISA = qw(Radius::Configurable);
use Radius::Configurable;
use Radius::Context;
use Digest::MD5;
use Socket;
use strict;

# Version numbers
$Radius::ServerTACACSPLUS::TAC_PLUSMAJOR_VERSION          = 0xc;
$Radius::ServerTACACSPLUS::TAC_PLUS_MINOR_VERSION_DEFAULT = 0;
$Radius::ServerTACACSPLUS::TAC_PLUSMINOR_VERSION_ONE      = 1;

# Request types
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN                = 1;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR                = 2;
$Radius::ServerTACACSPLUS::TAC_PLUS_ACCT                  = 3;

# Flags
$Radius::ServerTACACSPLUS::TAC_PLUS_UNENCRYPTED_FLAG      = 0x01; # Not really used!
$Radius::ServerTACACSPLUS::TAC_PLUS_SINGLE_CONNECT_FLAG   = 0x04;

# Authentication Start actions
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_LOGIN          = 1;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_CHPASS         = 2;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SENDPASS       = 3;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SENDAUTH       = 4;

# Authentication Start privelege levels
$Radius::ServerTACACSPLUS::TAC_PLUS_PRIV_LVL_MAX          = 0x0f;
$Radius::ServerTACACSPLUS::TAC_PLUS_PRIV_LVL_ROOT         = 0x0f;
$Radius::ServerTACACSPLUS::TAC_PLUS_PRIV_LVL_USER         = 0x01;
$Radius::ServerTACACSPLUS::TAC_PLUS_PRIV_LVL_MIN          = 0x00;

# Authentication Start authentication types
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_TYPE_ASCII     = 1;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_TYPE_PAP       = 2;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_TYPE_CHAP      = 3;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_TYPE_ARAP      = 4;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_TYPE_MSCHAP    = 5;

# Authentication Start service types
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_NONE       = 0;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_LOGIN      = 1;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_ENABLE     = 2;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_PPP        = 3;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_ARAP       = 4;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_PT         = 5;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_RCMD       = 6;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_X25        = 7;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_NASI       = 8;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_FWPROXY    =  9;

# Authentication Start status types
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_PASS    = 1;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_FAIL    = 2;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_GETDATA = 3;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_GETUSER = 4;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_GETPASS = 5;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_RESTART = 6;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_ERROR   = 7;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_FOLLOW  = 0x21;

# Authentication Start flags
$Radius::ServerTACACSPLUS::TAC_PLUS_REPLY_FLAG_NOECHO     = 1;

# Above value is correct but code uses this one
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_FLAG_NOECHO    = 1;

# Authentication Continue flags
$Radius::ServerTACACSPLUS::TAC_PLUS_CONTINUE_FLAG_ABORT     = 0x01;

# Authorization RESPONSE status types
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR_STATUS_PASS_ADD   = 0x01;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR_STATUS_PASS_REPL  = 0x02;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR_STATUS_FAIL       = 0x10;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR_STATUS_ERROR      = 0x11;
$Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR_STATUS_FOLLOW     = 0x21;

# Accounting flags
$Radius::ServerTACACSPLUS::TAC_PLUS_ACCT_MORE                = 0x01;
$Radius::ServerTACACSPLUS::TAC_PLUS_ACCT_START               = 0x02;
$Radius::ServerTACACSPLUS::TAC_PLUS_ACCT_STOP                = 0x04;
$Radius::ServerTACACSPLUS::TAC_PLUS_ACCT_WATCHDOG            = 0x08;

# Accounting reply status
$Radius::ServerTACACSPLUS::TAC_PLUS_ACCT_STATUS_SUCCESS      = 0x01;
$Radius::ServerTACACSPLUS::TAC_PLUS_ACCT_STATUS_ERROR        = 0x02;
$Radius::ServerTACACSPLUS::TAC_PLUS_ACCT_STATUS_FOLLOW       = 0x21;

# Map between Tacacs+ service types and Radius Service-Type
%Radius::ServerTACACSPLUS::service_to_service_type =
    (
     $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_LOGIN => 'Login-User',
     $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_ENABLE => 'Administrative-User',
     $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_SVC_PPP => 'Framed-User',
     );

#####################################################################
# This hash describes all the standards types of keywords understood by this
# class. If a keyword is not present in ConfigKeywords for this
# class, or any of its superclasses, Configurable will call sub keyword
# to parse the keyword
# See Configurable.pm for the list of permitted keywordtype
%Radius::ServerTACACSPLUS::ConfigKeywords = 
    (
     'Port'                 => 'string',
     'BindAddress'          => 'string',
     'MaxBufferSize'        => 'integer',
     'Key'                  => 'string',
     'AuthorizationAdd'     => 'stringarray',
     'AuthorizationReplace' => 'stringarray',
     'AuthorizationTimeout' => 'integer',
     'AddToRequest'         => 'string',
     'CommandAuth'	    => 'stringarray',
     );


#####################################################################
# Constructs a new handler
sub new
{
    my ($class, $file, @args) = @_;

    my $self = $class->SUPER::new($file, @args);

    $self->log($main::LOG_WARNING, "No Key defined for $class at '$main::config_file' line $.")
	if !defined $self->{Key};

    # Create a TCP socket to listen on, register it with select
    # Set up the TCP listener
    my $proto = getprotobyname('tcp');
    my $port = Radius::Util::get_port($self->{Port});
    &main::log($main::LOG_DEBUG, "Creating TACACSPLUS port $self->{BindAddress}:$port");
    my $s = do { local *FH };
    socket($s, Socket::PF_INET, Socket::SOCK_STREAM, $proto)
	|| $self->log($main::LOG_ERR,  "Could not create Server TACACSPLUS socket: $!");
    binmode($s); # Make safe in UTF environments
    setsockopt($s, Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1);
    my $bind_address = &Radius::Util::format_special($self->{BindAddress});
    bind($s, scalar Socket::sockaddr_in($port, Socket::inet_aton($bind_address)))
	|| $self->log($main::LOG_ERR,  "Could not bind Server TACACSPLUS socket: $!");
    listen($s, Socket::SOMAXCONN)
	|| $self->log($main::LOG_ERR,  "Could not listen on Server TACACSPLUS socket: $!");
    &Radius::Select::add_file
	(fileno($s), 1, undef, undef, 
	 \&handle_listen_socket_read, $s, $self);
    
}


#####################################################################
# Do per-instance default initialization
# This is called by Configurable during Configurable::new before
# the config file is parsed. Its a good place initialize instance 
# variables
# that might get overridden when the config file is parsed.
# Do per-instance default initialization. This is called after
# construction is complete
sub initialize
{
    my ($self) = @_;

    $self->SUPER::initialize;
    $self->{Port} = 49;
    $self->{MaxBufferSize} = 100000;
    $self->{BindAddress} = '0.0.0.0';
    $self->{AuthorizationTimeout} = 10; # seconds
}

#####################################################################
# This is called by Select::select whenever our listen socket
# becomes readable, which means someone is trying to connect to us
# We accept the new connection
sub handle_listen_socket_read
{
    my ($fileno, $listensocket, $self) = @_;

    # This could have been done with FileHandle, but this is much
    # more lightweight. It makes a reference to a TYPEGLOB
    # and Perl can use a typeglob ref as an IO handle
    my $newsocket = do { local *FH };
    
    if (!accept($newsocket, $listensocket))
    {
	$self->log($main::LOG_ERR,  "Could not accept on Tacacs listen socket: $!");
	return;
    }

    Radius::TacacsplusConnection->new
	($self, $newsocket,
	 MaxBufferSize        => $self->{MaxBufferSize},
	 Key                  => $self->{Key},
	 AuthorizationTimeout => $self->{AuthorizationTimeout},
	 AddToRequest         => $self->{AddToRequest},
	 Identifier           => $self->{Identifier});
}

#####################################################################
#####################################################################
#####################################################################
package Radius::TacacsplusConnection;

#####################################################################
sub new
{
    my ($class, $parent, $socket, %args) = @_;

    my $self = {%args};
    bless $self, $class;  


    $self->{parent} = $parent;
    $self->{socket} = $socket;

    $self->{peer} = getpeername($self->{socket})
	|| $parent->log($main::LOG_ERR,  "Could not get peer name on TacacsplusConnection socket: $!");
    if (length $self->{peer} != 16)
    {
	$parent->log($main::LOG_ERR,  "Strange TACACS peer socket address length");
	return;
    }
    my ($port, $peeraddr) = Socket::unpack_sockaddr_in($self->{peer});
    $self->{peerport} = $port;
    $self->{peeraddr} = Socket::inet_ntoa($peeraddr);

    $self->{inbuffer} = undef;
    $self->{outbuffer} = undef;
    
    $self->{Trace} = 0; # Default trace level

    $parent->log($main::LOG_DEBUG,  "New TacacsplusConnection created for $self->{peeraddr}:$self->{peerport}");

    &Radius::Select::add_file
	(fileno($self->{socket}), 1, undef, undef, 
	 \&handle_connection_socket_read, $self);
}

#####################################################################
# Called when more data can be read from the socket
sub handle_connection_socket_read
{
    my ($fileno, $self) = @_;

    # Append the next lot of bytes to the buffer
    if (sysread($self->{socket}, $self->{inbuffer}, 16384, length $self->{inbuffer}))
    {
	if (length $self->{inbuffer} >= 12)
	{
	    # Have the header at least
	    my ($version, $type, $seq_no, $flags, $session_id, $length) 
		= unpack('CCCCNN', $self->{inbuffer});
	    # Make some trivial checks on the request
	    if ($version != 0xc0 &&$version != 0xc1)
	    {
		# REVISIT: should send an ERROR message
		$self->{parent}->log($main::LOG_ERR, "TacacsplusConnection received a request for unsupported version $version. Disconnecting");
		
		$self->disconnect();
	    }
	    if ($length > $self->{MaxBufferSize})
	    {
		$self->{parent}->log($main::LOG_ERR, "TacacsplusConnection received a request with excessive length $length. Disconnecting");
		$self->disconnect();
	    }
	    
	    if (length $self->{inbuffer} >= $length + 12)
	    {
		# Have the entire request
		# Get, clear and handle this request
		my $request = substr($self->{inbuffer}, 0, $length+12, undef);
		$self->request($request);
	    }
	}
    }
    else
    {
	# Strange, nothing there, must be a disconnection error
	$self->disconnect();
    }
}

#####################################################################
# Called when more data can be written to the socket
sub handle_connection_socket_write
{
    my ($fileno, $self) = @_;

    $self->write_pending();
    # Dont need this callback any more if all the pending bytes
    # have been written
    &Radius::Select::remove_file
	(fileno($self->{socket}), undef, 1, undef)
	    if !length $self->{outbuffer};
}

#####################################################################
# Called when a complete request has been received
# Parse and process it
# Version has been checked
sub request
{
    my ($self, $request) = @_;

    my ($version, $type, $seq_no, $flags, $session_id, $length, $body) 
	= unpack('CCCCNNa*', $request);

    $self->{parent}->log($main::LOG_DEBUG, "TacacsplusConnection request $version, $type, $seq_no, $flags, $session_id, $length");
    # Need these during the reply phase
    $self->{version} = $version;
    $self->{last_seq_no} = $seq_no;
    $self->{session_id} = $session_id;

    # Maybe decrypt the payload
    $body = &crypt($session_id, $self->{Key}, $version, $seq_no, $body) if defined($self->{Key});

    my $x = unpack('H*', $body);

    if ($type == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN && $seq_no == 1)
    {
	$self->authentication_start($body);
    }
    elsif ($type == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN)
    {
	$self->authentication_continue($body);
    }
    elsif ($type == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR)
    {
	$self->authorization_request($body);
    }
    elsif ($type == $Radius::ServerTACACSPLUS::TAC_PLUS_ACCT)
    {
	$self->accounting_request($body);
    }
    # REVISIT: reset, error, etc
    else
    {
	$self->{parent}->log($main::LOG_WARNING, "TacacsplusConnection cant handle request type $type");
    }
}

#####################################################################
# Reversible TACACS+ encryption
sub crypt
{
    my ($session_id, $key, $version, $seq_no, $body) = @_;

    my ($res, $pad, $i);


    while ($i < length $body)
    {
	$pad = Digest::MD5::md5(pack('Na*CCa*', $session_id, $key, $version, $seq_no, $pad));
	$res .= substr($body, $i, 16) ^ $pad;
	$i += 16;
    }

    # Spec calls for encrypted data to be truncated to the length of
    # the cleartext message.
    $res = substr($res, 0, length($body));

    return $res;
}

#####################################################################
# Handle a TACACS+ authentication START request
sub authentication_start
{
    my ($self, $body) = @_;

    $self->{user} = undef;
    $self->{password} = undef;

    my ($action, $priv_lvl, $authen_type, $service, 
	$user_len, $port_len, $rem_addr_len, $data_len, 
	$fields) = unpack('CCCCCCCCa*', $body);
    # Decode the variable length fields
    my $i = 0;
    my $user     = substr($fields, $i, $user_len);     $i += $user_len;
    my $port     = substr($fields, $i, $port_len);     $i += $port_len;
    my $rem_addr = substr($fields, $i, $rem_addr_len); $i += $rem_addr_len;
    my $data     = substr($fields, $i, $data_len);     $i += $data_len;

    $self->{parent}->log($main::LOG_DEBUG, "TacacsplusConnection Authentication START $action, $authen_type, $service for $user, $port, $rem_addr");

    $self->{user} = $user;
    $self->{port} = $port;
    $self->{service} = $service;
    $self->{rem_addr} = $rem_addr;
    my $tp = $self->create_radius_request('Access-Request');

    if ($action == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_LOGIN
	&& $authen_type == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_TYPE_ASCII)
    {
	# Start of an ASCII login
	$self->{user} = $user;

	if (!length $user)
	{
	    $self->authentication_reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_GETUSER,
					0,
					'Username: ');
	}
	else
	{
	    # Ask for the password
	    $self->authentication_reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_GETPASS, $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_FLAG_NOECHO, 'Password: ');
	}
	# We should get an authentication CONTINUE soon.
	return;
	
    }
    elsif ($action == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_LOGIN
	&& $authen_type == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_TYPE_PAP)
    {
	# PAP login
	$tp->add_attr('User-Name', $user);
	$tp->add_attr('User-Password', $data);
	$tp->{DecodedPassword} = $data;
    }
    elsif ($action == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_LOGIN
	&& $authen_type == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_TYPE_CHAP)
    {
	# CHAP Login
	my ($chapid, $challenge, $result) = unpack('Ca16a16', $data);
	$tp->add_attr('User-Name', $user);
	$tp->add_attr('CHAP-Password', pack('Ca*', $chapid, $result));
	$tp->add_attr('CHAP-Challenge', $challenge);
    }
    else
    {
	$self->{parent}->log($main::LOG_WARNING, "TacacsplusConnection cant handle authentication action $action, type $authen_type");
	return;
    }

    $self->dispatch_radius_request($tp);
}

#####################################################################
# Create a standard fake Radius request
sub create_radius_request
{
    my ($self, $code) = @_;

    # Create a fake incoming radius request
    my $tp = Radius::Radius->new($main::dictionary);
    $tp->set_code($code);
    $tp->{RecvFrom} = $self->{peer};
    my @l = Socket::unpack_sockaddr_in($self->{peer});
    $tp->{RecvFromPort} = $l[0];
    $tp->{RecvFromAddress} = $l[1];
    $tp->{RecvTime} = time;
    $tp->{Client} = $self; # So you can use Client-Identifier check items
    $tp->set_authenticator(&Radius::Util::random_string(16));
    $tp->add_attr('NAS-IP-Address', $self->{peeraddr});
    $tp->add_attr('NAS-Port-Id', $self->{port}) if length $self->{port};
    $tp->add_attr('Calling-Station-Id', $self->{rem_addr}) if length $self->{rem_addr};
    $tp->add_attr('Service-Type', $Radius::ServerTACACSPLUS::service_to_service_type{$self->{service}}) if defined $Radius::ServerTACACSPLUS::service_to_service_type{$self->{service}};
    $tp->add_attr('Timestamp', time) if $code eq 'Accounting-Request';

    # Add arbitrary data to every request
    $tp->parse(&Radius::Util::format_special($self->{AddToRequest}))
	if (defined $self->{AddToRequest});

    # Arrange to call our reply function when we get a reply
    $tp->{replyFn} = [\&Radius::TacacsplusConnection::replyFn, $self];

    return $tp;
}

#####################################################################
# Dispatch a fake Radius request to the appropriate Handler
sub dispatch_radius_request
{
    my ($self, $tp) = @_;

    # Dump the fake radius request
    &main::log($main::LOG_DEBUG, "TACACSPLUS derived Radius request packet dump:\n" . $tp->dump)
	if (&main::willLog($main::LOG_DEBUG, $self->{parent}));

    # Now arrange for this fake radius request to be handled and find out the result
    my $originalname = $tp->get_attr('User-Name');
    $tp->{OriginalUserName} = $originalname;
    my ($userName, $realmName) = split(/@/, $originalname);
    my ($handler, $finder, $handled);
    foreach $finder (@Radius::Client::handlerFindFn)
    {
	if ($handler = &$finder($tp, $userName, $realmName))
	{
	    # Make sure the handler is updated with stats
	    push(@{$tp->{StatsTrail}}, \%{$handler->{Statistics}});
	    
	    # replyFn will be called from inside the handler when the
	    # reply is available
	    $handled = $handler->handle_request($tp);
	    last;
	}
    }
    $self->{parent}->log($main::LOG_WARNING, "TacacsplusConnection could not find a Handler")
	if !$handler;
}

#####################################################################
# Handle a TACACS+ authentication CONTINUE request
sub authentication_continue
{
    my ($self, $body) = @_;

    my ($user_msg_len, $data_len, $flags, $fields) = unpack('nnCa*', $body);
    # Decode the variable length fields
    my $i = 0;
    my $user_msg  = substr($fields, $i, $user_msg_len); $i += $user_msg_len;
    my $data      = substr($fields, $i, $data_len);     $i += $data_len;
    
    $self->{parent}->log($main::LOG_DEBUG, "TacacsplusConnection Authentication CONTINUE $flags, $user_msg, $data");

    if ($flags & $Radius::ServerTACACSPLUS::TAC_PLUS_CONTINUE_FLAG_ABORT)
    {
	$self->{parent}->log($main::LOG_WARN, "TacacsplusConnection Authentication CONTINUE aborted: $data");
	$self->disconnect();
    }

    if ($self->{last_status} == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_GETPASS)
    {
	$self->{password} = $user_msg;
    }
    elsif ($self->{last_status} == $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_GETUSER)
    {
	$self->{user} = $user_msg;
    }
    if (   defined $self->{password}
	&& length $self->{user})
    {
	# Create and dispatch a fake radius request. When the result becomes available
	# our replyFn will be called
	my $tp = $self->create_radius_request('Access-Request');
	$tp->add_attr('User-Name', $self->{user});
	$tp->add_attr('User-Password', $self->{password});
	$tp->{DecodedPassword} = $self->{password};
	$self->dispatch_radius_request($tp);
    }
    else
    {
	# Need more data
	return $self->authentication_reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_GETPASS,
					   $Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_FLAG_NOECHO,
					   'Password: ')
	    unless defined $self->{password};
	return $self->authentication_reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_GETUSER,
					   0,
					   'Username: ')
	    unless length $self->{user};
    }
}

#####################################################################
sub authorization_request
{
    my ($self, $body) = @_;

    my ($authen_method, $priv_lvl, $authen_type, $authen_service, 
	$user_len, $port_len, $rem_addr_len, $arg_cnt, $fields) = unpack('CCCCCCCCa*', $body);
    my @arg_len = unpack("C$arg_cnt", substr($fields, 0, $arg_cnt, undef));
    # Decode the variable length fields
    my $i = 0;
    my $user     = substr($fields, $i, $user_len);     $i += $user_len;
    my $port     = substr($fields, $i, $port_len);     $i += $port_len;
    my $rem_addr = substr($fields, $i, $rem_addr_len); $i += $rem_addr_len;
    # Unpack additional args
    my (@args, $j);
    for ($j = 0; $j < @arg_len; $j++)
    {
	$args[$j] = substr($fields, $i, $arg_len[$j]);     $i += $arg_len[$j];
	$args[$j] =~ s/\0+$//;  # Strip trailing NULs that some clients add
    }

    $self->{parent}->log($main::LOG_DEBUG, "TacacsplusConnection Authorization REQUEST $authen_method, $priv_lvl, $authen_type, $authen_service, $user, $port, $rem_addr, $arg_cnt, @args");

    # Routers want different kinds of responses for command authorization,
    # just a pass or fail with NO extra attributes sent with the response.
    # 
    # Cisco is nice and just sets the authen_method to NONE instead of 
    # TACPLUS, but the Juniper E-series sends it as TACPLUS.  Only other 
    # way to identify is that both send command authorization requests 
    # with a 'cmd=' value and a 'cmg-arg=' value (even if the command 
    # entered has simply <cr> as an argument.) 
    #
    # The draft mentions nothing about using NONE for command auth, so we'll
    # proceed with the cmd/cmg-arg pair to identify it.
    #
    # - Paul Schultz 10/07/03 

    my $cmd_auth = 1 if $args[1] =~ /^cmd\=/ && $args[2] =~ /^cmd-arg\=/;

    my ($cmd_auth_response, $cmd_auth_reason) = command_authorization($self, $user, @args) if $cmd_auth == 1 && defined $self->{parent}->{CommandAuth};

    $self->{user} = $user;
    $self->{port} = $port;
    $self->{rem_addr} = $rem_addr;

    # Recover the context and any radius reply to our earlier authentication request
    my $context = &Radius::Context::find("tacacs:$self->{user}");
    my $rp = $context->{rp} if $context;
    my @reply_pairs = $rp->get_attr('cisco-avpair') if $rp;

    if ( $cmd_auth_response == 1 ) {
	$self->authorization_reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR_STATUS_PASS_ADD);

    }
    elsif ( $cmd_auth_response == 2 ) {
	$self->authorization_reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR_STATUS_FAIL, $cmd_auth_reason);
    }
    elsif (defined $self->{parent}->{AuthorizationReplace})
    {
	$self->authorization_reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR_STATUS_PASS_REPL, 
				   undef, undef, 
				   @{$self->{parent}->{AuthorizationReplace}},
				   @{$self->{parent}->{AuthorizationAdd}},
				   @reply_pairs);
    }
    else
    {
	$self->authorization_reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR_STATUS_PASS_ADD, 
				   undef, undef, 
				   @{$self->{parent}->{AuthorizationAdd}},
				   @reply_pairs);
    }
}


#####################################################################
# authorizes per-command for cisco routers 
sub command_authorization {

    my ($self, $user, @auth_args) = @_;
    my ($cmd_auth_response, $cmd_auth_reason);

    my $auth_service = shift(@auth_args);
    my $auth_cmd = shift(@auth_args);

    # just does a basic top-down search of CommandAuth attributes
    # to try to find a match.. first match wins.
    foreach my $command ( @{$self->{parent}->{CommandAuth}}  ) {

	my ($action,$command,$response) = split(' ', $command, 3);
	my @commands = split(":", $command);
	my $command_value = "cmd=" . shift(@commands);
	my $command_arg = "cmd-arg=" . shift(@commands);

	# match command by regex !! does NOT check arguments yet
	if ( $auth_cmd =~ /^$command_value$/ ) {

	    if ( $action eq "permit" ) {
		$cmd_auth_response = 1;
	    }
	    else { 
		$cmd_auth_response = 2; 
		$cmd_auth_reason = $response;
	    }
	    last;
	}
    }
    return ($cmd_auth_response, $cmd_auth_reason);
}


#####################################################################
sub accounting_request
{
    my ($self, $body) = @_;

    my ($flags, $authen_method, $priv_lvl, $authen_type, $authen_service, 
	$user_len, $port_len, $rem_addr_len, $arg_cnt, $fields) = unpack('CCCCCCCCCa*', $body);
    my @arg_len = unpack("C$arg_cnt", substr($fields, 0, $arg_cnt, undef));
    # Decode the variable length fields
    my $i = 0;
    my $user     = substr($fields, $i, $user_len);     $i += $user_len;
    my $port     = substr($fields, $i, $port_len);     $i += $port_len;
    my $rem_addr = substr($fields, $i, $rem_addr_len); $i += $rem_addr_len;
    # Unpack additional args
    my (@args, $j);
    for ($j = 0; $j < @arg_len; $j++)
    {
	$args[$j] = substr($fields, $i, $arg_len[$j]); $i += $arg_len[$j];
	$args[$j] =~ s/\0+$//;  # Strip trailing NULs that some clients add
    }

    $self->{parent}->log($main::LOG_DEBUG, "TacacsplusConnection Accounting REQUEST $flags, $authen_method, $priv_lvl, $authen_type, $authen_service, $user, $port, $rem_addr, $arg_cnt, @args");

    $self->{port} = $port;
    $self->{rem_addr} = $rem_addr;

    my $tp = $self->create_radius_request('Accounting-Request');
    $tp->add_attr('User-Name', $user);
    # REVISIT: May need to do something a bit more interesting with these AV pairs
    foreach (@args)
    {
	$tp->add_attr('cisco-avpair', $_);
    }
    $self->dispatch_radius_request($tp);
    $self->accounting_reply($Radius::ServerTACACSPLUS::TAC_PLUS_ACCT_STATUS_SUCCESS);
}

#####################################################################
# This function is called automatically when an authentication request
# has been serviced. $tp->{rp} will have been set to the reply message
sub replyFn
{
    my ($tp, $self) = @_;

    my $reply_code = $tp->{rp}->code();  # The result of the request
    $self->{parent}->log($main::LOG_DEBUG, "TacacsplusConnection result $reply_code");
    if ($reply_code eq 'Access-Accept')
    {
	$self->authentication_reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_PASS, 0);

	# Sigh. Some TACACS clients (Cisco Aironet etc) create a new TCP session
	# for the authorisation phase. Therfore we cant cache the reply in $self.
	# So we have to create a context to hold the reply for a few seconds until
	# (maybe) an authorization REQUEST for this user arrives.
	my $context = Radius::Context->new("tacacs:$self->{user}", $self->{AuthorizationTimeout});
	$context->{rp} = $tp->{rp};
    }
    elsif ($reply_code eq 'Access-Reject')
    {
	$self->authentication_reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN_STATUS_FAIL, 0);
    }
}

#####################################################################
# Assemble and send and authentication reply message
sub authentication_reply
{
    my ($self, $status, $flags, $server_msg, $data) = @_;

    $self->{parent}->log($main::LOG_DEBUG, "TacacsplusConnection Authentication REPLY $status, $flags, $server_msg, $data ");
    $self->{last_status} = $status;
    my $body = pack('CCnna*a*', $status, $flags, 
		   length($server_msg), length($data),
		   $server_msg, $data);
    $self->reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHEN, $body);
}

#####################################################################
# Assemble and send and accounting reply message
sub accounting_reply
{
    my ($self, $status, $server_msg, $data) = @_;

    $self->{parent}->log($main::LOG_DEBUG, "TacacsplusConnection Accounting REPLY $status, $server_msg, $data ");
    my $body = pack('nnCa*a*',
		    length($server_msg), length($data),
		    $status,
		    $server_msg, $data);
    $self->reply($Radius::ServerTACACSPLUS::TAC_PLUS_ACCT, $body);
}

#####################################################################
# Assemble and send and authentication reply message
sub authorization_reply
{
    my ($self, $status, $server_msg, $data, @args) = @_;

    my $nargs = @args;
    my $arglenarray = pack('C*', map {length $_} @args);
    $self->{parent}->log($main::LOG_DEBUG, "TacacsplusConnection Authorization RESPONSE $status, $server_msg, $data, @args");
    my $body = pack("CCnna*a*a*a*", $status, $nargs,
		   length($server_msg), length($data), 
		    $arglenarray,
		   $server_msg, $data,
		    join('', @args));
    $self->reply($Radius::ServerTACACSPLUS::TAC_PLUS_AUTHOR, $body);
}

#####################################################################
# Assemble a complete TACACS+ message, and encrypt the body if required
sub reply
{
    my ($self, $type, $body) = @_;

    my $session_id = $self->{session_id};
    my $version = $self->{version};
    my $seq_no = $self->{last_seq_no} + 1;
    my $flags;

    # check if we're doing encryption
    if ( !defined($self->{Key}) )
    {
	$flags = $Radius::ServerTACACSPLUS::TAC_PLUS_UNENCRYPTED_FLAG;
    }

    $body = &crypt($session_id, $self->{Key}, $version, $seq_no, $body) if defined($self->{Key});
    
    my $msg = pack('CCCCNNa*', 
		   $version, 
		   $type,
		   $seq_no,
		   $flags,
		   $session_id,
		   length($body),
		   $body);
    $self->write($msg);
}

#####################################################################
sub write
{
    my ($self, $s) = @_;

    $self->{outbuffer} .= $s;
    if (length $self->{outbuffer} > $self->{MaxBufferSize})
    {
	$self->{parent}->log($main::LOG_ERR, "TacacsplusConnection MaxBufferSize exceeded, disconnecting");

	$self->disconnect();
    }
    else
    {
	$self->write_pending();
    }
}

#####################################################################
sub write_pending
{
    my ($self) = @_;

    # BUG ALERT what hapens if the syswrite blocks?
    my $written = syswrite($self->{socket}, $self->{outbuffer}, 
			   length $self->{outbuffer});
    if (!defined $written)
    {
	$self->{parent}->log($main::LOG_ERR, "TacacsplusConnection write error, disconnecting: $!");

	$self->disconnect();
    }
    else
    {
	# Remove the bytes that have been written already
	substr($self->{outbuffer}, 0, $written, '');

	# Anything left? it was a partial write, need to
	# get control when the socket is writeable again
	if (length $self->{outbuffer})
	{
	    &Radius::Select::add_file
		(fileno($self->{socket}), undef, 1, undef, 
		 \&handle_connection_socket_write, $self);
	}
    }
}

#####################################################################
sub disconnect
{
    my ($self) = @_;

    # Deleting any references to this TacacsConnection will
    # cause it to be destroyed    
    &Radius::Select::remove_file(fileno($self->{socket}), 1, 1, 1);

    $self->{parent}->log($main::LOG_DEBUG,  "TacacsplusConnection disconnected from $self->{peeraddr}:$self->{peerport}");
}

1;

Reply via email to