############################################################################# # # # Radius Client module for Perl 5 # # # # Written by Carl Declerck , (c)1997 # # All Rights Reserved. See the Perl Artistic License for copying & usage # # policy. # # # # Modified by Olexander Kapitanenko , # # Andrew Zhilenko , 2002-2007. # # # # See the file 'Changes' in the distrution archive. # # # ############################################################################# # $Id: Radius.pm,v 1.17 2007/02/20 06:15:04 andrew Exp $ package Authen::Radius; use strict; use FileHandle; use IO::Socket; use IO::Select; use Digest::MD5; use Data::Dumper; use Data::HexDump; use vars qw($VERSION @ISA @EXPORT); require Exporter; @ISA = qw(Exporter); @EXPORT = qw(ACCESS_REQUEST ACCESS_ACCEPT ACCESS_REJECT ACCOUNTING_REQUEST ACCOUNTING_RESPONSE ACCOUNTING_STATUS DISCONNECT_REQUEST); $VERSION = '0.13'; my (%dict_id, %dict_name, %dict_val, %dict_vendor_id, %dict_vendor_name ); my ($request_id) = $$ & 0xff; # probably better than starting from 0 my ($radius_error) = 'ENONE'; my $debug = 0; # # we'll need to predefine these attr types so we can do simple password # verification without having to load a dictionary # $dict_id{'not defined'}{1}{'type'} = 'string'; # set 'username' attr type to string $dict_id{'not defined'}{2}{'type'} = 'string'; # set 'password' attr type to string $dict_id{'not defined'}{4}{'type'} = 'ipaddr'; # set 'NAS-IP-Address' attr type to string use constant ACCESS_REQUEST => 1; use constant ACCESS_ACCEPT => 2; use constant ACCESS_REJECT => 3; use constant ACCOUNTING_REQUEST => 4; use constant ACCOUNTING_RESPONSE => 5; use constant ACCOUNTING_STATUS => 6; use constant DISCONNECT_REQUEST => 40; sub new { my $class = shift; my %h = @_; my ($host, $port, $service); my $self = {}; bless $self, $class; $self->set_error; $debug = $h{Debug}; return $self->set_error('ENOHOST') unless $h{'Host'}; ($host, $port) = split(/:/, $h{'Host'}); $service = $h{'Service'} ? $h{'Service'} : 'radius'; $port = getservbyname($service, 'udp') unless $port; unless ($port) { my %services = ( radius => 1645, radacct => 1646, 'radius-acct' => 1813 ); if (exists($services{$service})) { $port = $services{$service}; } else { return $self->set_error('EBADSERV'); } } $self->{'timeout'} = $h{'TimeOut'} ? $h{'TimeOut'} : 5; $self->{'secret'} = $h{'Secret'}; print STDERR "Using Radius server $host:$port\n" if $debug; $self->{'sock'} = new IO::Socket::INET( PeerAddr => $host, PeerPort => $port, Type => SOCK_DGRAM, Proto => 'udp', TimeOut => $self->{'timeout'} ) or return $self->set_error('ESOCKETFAIL'); $self; } sub send_packet { my ($self, $type) = @_; my ($data); my $length = 20 + length($self->{'attributes'}); $self->set_error; if ($type == ACCOUNTING_REQUEST || $type == DISCONNECT_REQUEST) { $self->{'authenticator'} = "\0" x 16; $self->{'authenticator'} = $self->calc_authenticator($type, $request_id, $length) } else { $self->gen_authenticator unless defined $self->{'authenticator'}; } $data = pack('C C n', $type, $request_id, $length) . $self->{'authenticator'} . $self->{'attributes'}; $request_id = ($request_id + 1) & 0xff; if ($debug) { print STDERR "Sending request:\n"; print STDERR HexDump($data); } $self->{'sock'}->send ($data) || $self->set_error('ESENDFAIL'); } sub recv_packet { my ($self) = @_; my ($data, $type, $id, $length, $auth, $sh); $self->set_error; $sh = new IO::Select($self->{'sock'}) or return $self->set_error('ESELECTFAIL'); $sh->can_read($self->{'timeout'}) or return $self->set_error('ETIMEOUT'); $self->{'sock'}->recv ($data, 65536) or return $self->set_error('ERECVFAIL'); if ($debug) { print STDERR "Received response:\n"; print STDERR HexDump($data); } ($type, $id, $length, $auth, $self->{'attributes'}) = unpack('C C n a16 a*', $data); return $self->set_error('EBADAUTH') if $auth ne $self->calc_authenticator($type, $id, $length); $type; } sub check_pwd { my ($self, $name, $pwd, $nas) = @_; $self->clear_attributes; $self->add_attributes ( { Name => 1, Value => $name, Type => 'string' }, { Name => 2, Value => $pwd, Type => 'string' }, { Name => 4, Value => $nas || '127.0.0.1', Type => 'ipaddr' } ); $self->send_packet(ACCESS_REQUEST); my $rcv = $self->recv_packet(); return (defined($rcv) and $rcv == ACCESS_ACCEPT); } sub clear_attributes { my ($self) = @_; $self->set_error; delete $self->{'attributes'}; 1; } sub get_attributes { my ($self) = @_; my ($vendor, $vendor_id, $id, $length, $value, $type, $rawvalue, @a); my ($attrs) = $self->{'attributes'}; $self->set_error; my $vendor_specific = $dict_name{'Vendor-Specific'}{'id'}; while (length($attrs)) { ($id, $length, $attrs) = unpack('C C a*', $attrs); ($rawvalue, $attrs) = unpack('a' . ($length - 2) . ' a*', $attrs); if ( defined($vendor_specific) and $id == $vendor_specific ) { ($vendor_id, $id, $length, $rawvalue) = unpack('N C C a*', $rawvalue); $vendor = defined $dict_vendor_id{$vendor_id}{'name'} ? $dict_vendor_id{$vendor_id}{'name'} : $vendor_id; } else { $vendor = 'not defined'; } $type = $dict_id{$vendor}{$id}{'type'} || ''; if ($type eq "string") { if ($id == 2 && $vendor eq 'not defined' ) { $value = ''; } else { $value = $rawvalue; } } elsif ($type eq "integer") { $value = unpack('N', $rawvalue); $value = $dict_val{$id}{$value}{'name'} if defined $dict_val{$id}{$value}{'name'}; } elsif ($type eq "ipaddr") { $value = inet_ntoa($rawvalue); } elsif ($type eq "avpair") { $value = $rawvalue; $value =~ s/^.*=//; } elsif ($type eq 'sublist') { # never got a chance to test it, since it seems that Digest attributes only come from clients my ($subid, $subvalue, $sublength, @values); $value = ''; my $subrawvalue = $rawvalue; while (length($subrawvalue)) { ($subid, $sublength, $subrawvalue) = unpack('C C a*', $subrawvalue); ($subvalue, $subrawvalue) = unpack('a' . ($sublength - 2) . ' a*', $subrawvalue); my $subname = $dict_val{$id}->{$subid}->{'name'}; push @values, "$subname = \"$subvalue\""; } $value = join("; ", @values); } push (@a, { 'Name' => defined $dict_id{$vendor}{$id}{'name'} ? $dict_id{$vendor}{$id}{'name'} : $id, 'Code' => $id, 'Value' => $value, 'RawValue' => $rawvalue, 'Vendor' => $vendor } ); } return @a; } sub add_attributes { my ($self, @a) = @_; my ($a, $vendor, $id, $type, $value); $self->set_error; for $a (@a) { $id = defined $dict_name{$a->{'Name'}}{'id'} ? $dict_name{$a->{'Name'}}{'id'} : int($a->{'Name'}); $type = defined $a->{'Type'} ? $a->{'Type'} : $dict_name{$a->{'Name'}}{'type'}; $vendor = defined $a->{'Vendor'} ? ( defined $dict_vendor_name{ $a->{'Vendor'} }{'id'} ? $dict_vendor_name{ $a->{'Vendor'} }{'id'} : int($a->{'Vendor'}) ) : ( defined $dict_name{$a->{'Name'}}{'vendor'} ? $dict_vendor_name{ $dict_name{$a->{'Name'}}{'vendor'} }{'id'} : 'not defined' ); if ($type eq "string") { $value = $a->{'Value'}; if ($id == 2 && $vendor eq 'not defined' ) { $self->gen_authenticator(); $value = $self->encrypt_pwd($value); } $value = substr($value, 0, 253); } elsif ($type eq "integer") { my $enc_value; if ( defined $dict_val{$id}{$a->{'Value'}}{'id'} ) { $enc_value = $dict_val{$id}{$a->{'Value'}}{'id'}; } else { $enc_value = int($a->{'Value'}); } $value = pack('N', $enc_value); } elsif ($type eq "ipaddr") { $value = inet_aton($a->{'Value'}); } elsif ($type eq "avpair") { $value = $a->{'Name'}.'='.$a->{'Value'}; $value = substr($value, 0, 253); } elsif ($type eq 'sublist') { # Digest attributes look like: # Digest-Attributes = 'Method = "REGISTER"' my $digest = $a->{'Value'}; my @pairs; if (ref($digest)) { next unless ref($digest) eq 'HASH'; foreach my $key (keys %{$digest}) { push @pairs, [ $key => $digest->{$key} ]; } } else { # string foreach my $z (split(/\"\; /, $digest)) { my ($subname, $subvalue) = split(/\s+=\s+\"/, $z, 2); $subvalue =~ s/\"$//; push @pairs, [ $subname => $subvalue ]; } } $value = ''; foreach my $da (@pairs) { my ($subname, $subvalue) = @{$da}; my $subid = $dict_val{$id}->{$subname}->{'id'}; next unless defined($subid); $value .= pack('C C', $subid, length($subvalue) + 2) . $subvalue; } } else { next; } print STDERR "Adding attribute $a->{Name} ($id) with value '$a->{Value}'\n" if $debug; if ( $vendor eq 'not defined' ) { $self->{'attributes'} .= pack('C C', $id, length($value) + 2) . $value; } else { $value = pack('N C C', $vendor, $id, length($value) + 2) . $value; $self->{'attributes'} .= pack('C C', $dict_name{'Vendor-Specific'}{'id'}, length($value) + 2) . $value; } } return 1; } sub calc_authenticator { my ($self, $type, $id, $length) = @_; my ($hdr, $ct); $self->set_error; $hdr = pack('C C n', $type, $id, $length); $ct = Digest::MD5->new; $ct->add ($hdr, $self->{'authenticator'}, $self->{'attributes'}, $self->{'secret'}); $ct->digest(); } sub gen_authenticator { my ($self) = @_; my ($ct); $self->set_error; $ct = Digest::MD5->new; # the following could be improved a lot $ct->add (sprintf("%08x%04x", time, $$), $self->{'attributes'} || ''); $self->{'authenticator'} = $ct->digest(); } sub encrypt_pwd { my ($self, $pwd) = @_; my ($i, $ct, @pwdp, @encrypted); $self->set_error; $ct = Digest::MD5->new(); my $non_16 = length($pwd) % 16; $pwd .= "\0" x (16 - $non_16) if $non_16; @pwdp = unpack('a16' x (length($pwd) / 16), $pwd); for $i (0..$#pwdp) { my $authent = $i == 0 ? $self->{'authenticator'} : $encrypted[$i - 1]; $ct->add($self->{'secret'}, $authent); $encrypted[$i] = $pwdp[$i] ^ $ct->digest(); } return join('',@encrypted); } use vars qw(%included_files); sub load_dictionary { shift; my ($file) = @_; my ($fh, $cmd, $name, $id, $type, $vendor); unless ($file) { $file = "/etc/raddb/dictionary"; } # prevent infinite loop in the include files return undef if exists($included_files{$file}); $included_files{$file} = 1; $fh = new FileHandle($file) or die "Can't open dictionary '$file' ($!)\n"; print STDERR "Loading dictionary $file\n" if $debug; while (<$fh>) { chomp; ($cmd, $name, $id, $type, $vendor) = split(/\s+/); next if (!$cmd || $cmd =~ /^#/); if (lc($cmd) eq 'attribute') { if( !$vendor ) { $dict_id{'not defined'}{$id}{'name'} = $name; $dict_id{'not defined'}{$id}{'type'} = $type; } else { $dict_id{$vendor}{$id}{'name'} = $name; $dict_id{$vendor}{$id}{'type'} = $type; } $dict_name{$name}{'id'} = $id; $dict_name{$name}{'type'} = $type; $dict_name{$name}{'vendor'} = $vendor if $vendor; } elsif (lc($cmd) eq 'value') { next unless exists($dict_name{$name}); $dict_val{$dict_name{$name}->{'id'}}->{$type}->{'name'} = $id; $dict_val{$dict_name{$name}->{'id'}}->{$id}->{'id'} = $type; } elsif (lc($cmd) eq 'vendor') { $dict_vendor_name{$name}{'id'} = $id; $dict_vendor_id{$id}{'name'} = $name; } elsif (lc($cmd) eq '$include') { my @path = split("/", $file); pop @path; # remove the filename at the end my $path = ( $name =~ /^\// ) ? $name : join("/", @path, $name); load_dictionary('', $path); } } $fh->close; 1; } sub set_error { my ($self, $error) = @_; $radius_error = $self->{'error'} = defined $error ? $error : 'ENONE'; undef; } sub get_error { my ($self) = @_; $self->{'error'}; } sub strerror { my ($self, $error) = @_; my %errors = ( 'ENONE', 'none', 'ESELECTFAIL', 'select creation failed', 'ETIMEOUT', 'timed out waiting for packet', 'ESOCKETFAIL', 'socket creation failed', 'ENOHOST', 'no host specified', 'EBADAUTH', 'bad response authenticator', 'ESENDFAIL', 'send failed', 'ERECVFAIL', 'receive failed', 'EBADSERV', 'unrecognized service' ); return $errors{$radius_error} unless ref($self); $errors{defined $error ? $error : $self->{'error'}}; } 1; __END__ =head1 NAME Authen::Radius - provide simple Radius client facilities =head1 SYNOPSIS use Authen::Radius; $r = new Authen::Radius(Host => 'myserver', Secret => 'mysecret'); print "auth result=", $r->check_pwd('myname', 'mypwd'), "\n"; $r = new Authen::Radius(Host => 'myserver', Secret => 'mysecret'); Authen::Radius->load_dictionary(); $r->add_attributes ( { Name => 'User-Name', Value => 'myname' }, { Name => 'Password', Value => 'mypwd' }, # RFC 2865 http://www.ietf.org/rfc/rfc2865.txt calls this attribute # User-Password. Check your local RADIUS dictionary to find # out which name is used on your system # { Name => 'User-Password', Value => 'mypwd' }, { Name => 'h323-return-code', Value => '0' }, # Cisco AV pair { Name => 'Digest-Attributes', Value => { Method => 'REGISTER' } } ); $r->send_packet(ACCESS_REQUEST) and $type = $r->recv_packet(); print "server response type = $type\n"; for $a ($r->get_attributes()) { print "attr: name=$a->{'Name'} value=$a->{'Value'}\n"; } =head1 DESCRIPTION The C module provides a simple class that allows you to send/receive Radius requests/responses to/from a Radius server. =head1 CONSTRUCTOR =over 4 =item new ( Host => HOST, Secret => SECRET [, TimeOut => TIMEOUT] [,Service => SERVICE] [, Debug => Bool]) Creates & returns a blessed reference to a Radius object, or undef on failure. Error status may be retrieved with C (errorcode) or C (verbose error string). The default C is C, the alternative is C. If you do not specify port in the C as a C, then port specified in your F will be used. If there is nothing there, and you did not specify port either then default is 1645 for C and 1813 for C. Optional parameter C with a Perl "true" value turns on debugging (verbose mode). =back =head1 METHODS =over 4 =item load_dictionary ( [ DICTIONARY ] ) Loads the definitions in the specified Radius dictionary file (standard Livingston radiusd format). Tries to load 'C' when no argument is specified, or dies. NOTE: you need to load valid dictionary if you plan to send Radius requests with other attributes than just C/C. =item check_pwd ( USERNAME, PASSWORD [,NASIPADDRESS] ) Checks with the Radius server if the specified C is valid for user C. Unless C is soecified, 127.0.0.1 will be placed in the NAS-IP-Address attribute. This method is actually a wrapper for subsequent calls to C, C, C and C. It returns 1 if the C is correct, or undef otherwise. =item add_attributes ( { Name => NAME, Value => VALUE [, Type => TYPE] [, Vendor => VENDOR] }, ... ) Adds any number of Radius attributes to the current Radius object. Attributes are specified as a list of anon hashes. They may be Cd with their dictionary name (provided a dictionary has been loaded first), or with their raw Radius attribute-type values. The C pair should be specified when adding attributes that are not in the dictionary (or when no dictionary was loaded). Values for C can be 'C', 'C', 'C' or 'C'. =item get_attributes Returns a list of references to anon hashes with the following key/value pairs : { Name => NAME, Code => RAWTYPE, Value => VALUE, RawValue => RAWVALUE, Vendor => VENDOR }. Each hash represents an attribute in the current object. The C and C pairs will contain values as translated by the dictionary (if one was loaded). The C and C pairs always contain the raw attribute type & value as received from the server. =item clear_attributes Clears all attributes for the current object. =item send_packet ( REQUEST_TYPE ) Packs up a Radius packet based on the current secret & attributes and sends it to the server with a Request type of C. Exported C methods are 'C', 'C' , 'C', 'C', 'C', and 'C'. Returns the number of bytes sent, or undef on failure. =item recv_packet Receives a Radius reply packet. Returns the Radius Reply type (see possible values for C in method C) or undef on failure. Note that failure may be due to a failed recv() or a bad Radius response authenticator. Use C to find out. =item get_error Returns the last C for the current object. Errorcodes are one-word strings always beginning with an 'C'. =item strerror ( [ ERRORCODE ] ) Returns a verbose error string for the last error for the current object, or for the specified C. =back =head1 AUTHOR Carl Declerck - original design Alexander Kapitanenko and Andrew Zhilenko - later modifications. Andrew Zhilenko is a current module's maintaner at CPAN. =cut