Skip to content
Snippets Groups Projects
Packet.pm 17.6 KiB
Newer Older
Nigel Kukard's avatar
Nigel Kukard committed
package Radius::Packet;

use strict;
require Exporter;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK $VSA);
@ISA       = qw(Exporter);
@EXPORT    = qw(auth_resp auth_acct_verify auth_req_verify);
@EXPORT_OK = qw( );

$VERSION = '1.55';

$VSA = 26;			# Type assigned in RFC2138 to the 
				# Vendor-Specific Attributes

# Be sure our dictionaries are current
use Radius::Dictionary 1.50;
use Carp;
use Socket;
use Digest::MD5;

my (%unkvprinted, %unkgprinted);

sub new {
  my ($class, $dict, $data) = @_;
  my $self = { unknown_entries => 1 };
  bless $self, $class;
  $self->set_dict($dict) if defined($dict);
  $self->unpack($data) if defined($data);
  return $self;
}

# Set the dictionary
sub set_dict {
  my ($self, $dict) = @_;
  $self->{Dict} = $dict;
}

# Functions for accessing data structures
sub code          { $_[0]->{Code};          				}
sub identifier    { $_[0]->{Identifier};    				}
sub authenticator { $_[0]->{Authenticator}; 				}

sub set_code          { $_[0]->{Code} = $_[1];          		}
sub set_identifier    { $_[0]->{Identifier} = $_[1];    		}
sub set_authenticator { $_[0]->{Authenticator} = substr($_[1] 
							. "\x0" x 16, 
							0, 16); 	}

sub vendors      { keys %{$_[0]->{VSAttributes}};			}
sub vsattributes { keys %{$_[0]->{VSAttributes}->{$_[1]}};		}
sub vsattr       { $_[0]->{VSAttributes}->{$_[1]}->{$_[2]};		}
sub set_vsattr   { 
    my ($self, $vendor, $name, $value, $rewrite_flag, $rawValue) = @_;
Nigel Kukard's avatar
Nigel Kukard committed
    $self->{VSAttributes}->{$vendor} = {} unless exists($self->{VSAttributes}->{$vendor});
    my $attr = $self->{VSAttributes}->{$vendor};

    if ($rewrite_flag) {
	my $found = 0;

	if (exists($attr->{$name})) {
	    $found = $#{$attr->{$name}} + 1;
        }

	if ($found == 1) {
	    $attr->{$name}[0] = $value;
	    return;
	}    
    }

    # Check if we should be adding the raw value or not
    if (defined($rawValue)) {
	    push @{$attr->{$name}}, $value, $rawValue;
    } else {
	    push @{$attr->{$name}}, $value;
    }
Nigel Kukard's avatar
Nigel Kukard committed
}

sub unset_vsattr {
    my ($self, $vendor, $name) = @_;

    delete($self->{VSAttributes}->{$name});
}

sub show_unknown_entries { $_[0]->{unknown_entries} = $_[1]; 		}

sub set_attr 
{
    my ($self, $name, $value, $rewrite_flag, $rawValue) = @_;
Nigel Kukard's avatar
Nigel Kukard committed
    my ($push, $pos );

    $push = 1 unless $rewrite_flag;

    if ($rewrite_flag) {
        my $found = 0;
        my @attr = $self->_attributes;

        for (my $i = 0; $i <= $#attr; $i++ ) {
            if ($attr[$i][0] eq $name) {
                $found++;
                $pos = $i;
            }
        }

        if ($found > 1) {
            $push = 1;
        } elsif ($found) {
            $attr[$pos][0] = $name;
            $attr[$pos][1] = $value;
            $attr[$pos][2] = $rawValue;
Nigel Kukard's avatar
Nigel Kukard committed
            $self->_set_attributes( \@attr );
            return;
        } else {
            $push = 1;
        }
    }

    $self->_push_attr( $name, $value, $rawValue ) if $push;
Nigel Kukard's avatar
Nigel Kukard committed
}

sub attr
{
    my ($self, $name ) = @_;
    
    my @attr = $self->_attributes;
    
    for (my $i = $#attr; $i >= 0; $i-- ) {
        return $attr[$i][1] if $attr[$i][0] eq $name;
    }
    return;
}

sub rawattr
{
    my ($self, $name ) = @_;
    
    my @attr = $self->_attributes;
    
    for (my $i = $#attr; $i >= 0; $i-- ) {
	# Check if this is the attr we're after
	if ($attr[$i][0] eq $name) {
		# If it is, return the raw attribute if it exists, else return the nicer dict one
		return defined($attr[$i][2]) ? $attr[$i][2] : $attr[$i][1];
	}
Nigel Kukard's avatar
Nigel Kukard committed
sub attributes {
    my ($self) = @_;
    
    my @attr = $self->_attributes;
    my @attriblist = ();
    for (my $i = $#attr; $i >= 0; $i-- ) {
        push @attriblist, $attr[$i][0];
    }
    return @attriblist;
}

sub unset_attr 
{
    my ($self, $name, $value ) = @_;
    
    my $found;
    my @attr = $self->_attributes;

    for (my $i = 0; $i <= $#attr; $i++ ) {
        if ( $name eq $attr[$i][0] && $value eq $attr[$i][1])
	{
            $found = 1;
	    if ( $#attr == 0 ) {
		# no more attributes left on the stack
		$self->_set_attributes( [ ] );
	    } else {
		splice @attr, $i, 1;
		$self->_set_attributes( \@attr );
	    }
            return 1;
        }
    }
    return 0;
}

# XXX - attr_slot is deprecated - Use attr_slot_* instead
sub attr_slot		{ attr_slot_val($@); }

sub attr_slots { scalar ($_[0]->_attributes); }

sub attr_slot_name
{ 
    my $self = shift;
    my $slot = shift;
    my @stack = $self->_attributes;

    return unless exists $stack[$slot];
    return unless exists $stack[$slot]->[0];
    $stack[$slot]->[0];
}

sub attr_slot_val
{ 
    my $self = shift;
    my $slot = shift;
    my @stack = $self->_attributes;

    return unless exists $stack[$slot];
    return unless exists $stack[$slot]->[0];
    $stack[$slot]->[1];
}

sub unset_attr_slot {
    my ($self, $position ) = @_;

    my @attr = $self->_attributes;

    if ( not $position > $#attr ) {
        splice @attr, $position, 1;
        $self->_set_attributes( \@attr );
        return 1;
    } else {
        return;
    }

}

# 'Attributes' is now array of arrays, so that we can have multiple
# Proxy-State values in the order in which they were added,
# as specified in RFC 2865
sub _attributes     { @{ $_[0]->{Attributes} || [] }; }
sub _set_attributes { $_[0]->{Attributes} = $_[1]; }
sub _push_attr      { push @{ $_[0]->{Attributes} }, [ $_[1], $_[2], $_[3] ]; }
Nigel Kukard's avatar
Nigel Kukard committed

# Decode the password
sub password {
  my ($self, $secret, $attr) = @_;
  my $lastround = $self->authenticator;
  my $pwdin = $self->attr($attr || "User-Password");
  my $pwdout = ""; # avoid possible undef warning
  for (my $i = 0; $i < length($pwdin); $i += 16) {
    $pwdout .= substr($pwdin, $i, 16) ^ Digest::MD5::md5($secret . $lastround);
    $lastround = substr($pwdin, $i, 16);
  }
  $pwdout =~ s/\000*$// if $pwdout;
    substr($pwdout,length($pwdin)) = "" 
	unless length($pwdout) <= length($pwdin);
  return $pwdout;
}

# Encode the password
sub set_password {
  my ($self, $pwdin, $secret, $attribute) = @_;
  $attribute ||= 'User-Password';
  my $lastround = $self->authenticator;
  my $pwdout = ""; # avoid possible undef warning
  $pwdin .= "\000" x (15-(15 + length $pwdin)%16);     # pad to 16n bytes

  for (my $i = 0; $i < length($pwdin); $i += 16) {
    $lastround = substr($pwdin, $i, 16) 
      ^ Digest::MD5::md5($secret . $lastround);
    $pwdout .= $lastround;
  }
  $self->set_attr($attribute => $pwdout, 1);
}

# Set response authenticator in binary packet
sub auth_resp {
  my $new = $_[0];
  substr($new, 4, 16) = Digest::MD5::md5($_[0] . $_[1]);
  return $new;
}

# Verify the authenticator in a packet
sub auth_acct_verify { auth_req_verify(@_, "\x0" x 16); }
sub auth_req_verify
{
    my ($packet, $secret, $prauth) = @_;

    return 1 if Digest::MD5::md5(substr($packet, 0, 4) . $prauth 
				 . substr($packet, 20) . $secret)
	eq substr($packet, 4, 16);
    return;
}

# Utility functions for printing/debugging
sub pdef { defined $_[0] ? $_[0] : "UNDEF"; }
sub pclean {
  my $str = $_[0];
  $str =~ s/([\044-\051\133-\136\140\173-\175])/'\\' . $1/ge;
  $str =~ s/([\000-\037\177-\377])/sprintf('\x{%x}', ord($1))/ge;
  return $str;
}

sub dump {
    print str_dump(@_);
}

sub str_dump {
  my $self = shift;
  my $ret = '';
  $ret .= "*** DUMP OF RADIUS PACKET ($self)\n";
  $ret .= "Code:       ". pdef($self->{Code}). "\n";
  $ret .= "Identifier: ". pdef($self->{Identifier}). "\n";
  $ret .= "Authentic:  ". pclean(pdef($self->{Authenticator})). "\n";
  $ret .= "Attributes:\n";

  for (my $i = 0; $i < $self->attr_slots; ++$i)
  {
      $ret .= sprintf("  %-20s %s\n", $self->attr_slot_name($i) . ":" , 
		    pclean(pdef($self->attr_slot_val($i))));
  }

  foreach my $vendor ($self->vendors) {
    $ret .= "VSA for vendor $vendor\n";
    foreach my $attr ($self->vsattributes($vendor)) {
      $ret .= sprintf("    %-20s %s\n", $attr . ":" ,
		      pclean(join("|", @{$self->vsattr($vendor, $attr)})));
    }
  }
  $ret .= "*** END DUMP\n";
  return $ret;
}

sub pack {
  my $self = shift;
  my $hdrlen = 1 + 1 + 2 + 16;    # Size of packet header
  my $p_hdr  = "C C n a16 a*";    # Pack template for header
  my $p_attr = "C C a*";          # Pack template for attribute
  my $p_vsa  = "C C N C C a*";	  # VSA

  # XXX - The spec says that a
  # 'Vendor-Type' must be included
  # but there are no documented definitions
  # for this! We'll simply skip this value

  my $p_vsa_3com  = "C C N N a*";    

  my %codes  = $self->{Dict}->packet_numbers();
  my $attstr = "";                # To hold attribute structure
  # Define a hash of subroutine references to pack the various data types
  my %packer = (
		"octets" => sub { return $_[0]; },
		"string" => sub { return $_[0]; },
		"ipv6addr" => sub { return $_[0]; },
		"date" => sub { return $_[0]; },
		"ifid" => sub { return $_[0]; },
		"integer" => sub {
		    return pack "N",
		    (
		     defined $self->{Dict}->attr_has_val($_[1]) &&
		     defined $self->{Dict}->val_num(@_[1, 0])
		     ) 
			? $self->{Dict}->val_num(@_[1, 0]) 
			: $_[0];
		},
		"ipaddr" => sub {
		    return inet_aton($_[0]);
		},
		"time" => sub {
		    return pack "N", $_[0];
		},
		"date" => sub {
		    return pack "N", $_[0];
		},
		"tagged-string" => sub { 
		    return $_[0]; 
		},
		"tagged-integer" => sub {
		    return $_[0];
		},
		"tagged-ipaddr" => sub {
		    my ($tag,$val)=unpack "C a*",$_[0];
		    return pack "C N" , $tag , inet_aton($val);
		});

  my %vsapacker = (
		   "octets" => sub { return $_[0]; },
		   "string" => sub { return $_[0]; },
		   "ipv6addr" => sub { return $_[0]; },
		   "date" => sub { return $_[0]; },
		   "ifid" => sub { return $_[0]; },
		   "integer" => sub {
		       my $vid = $self->{Dict}->vendor_num($_[2]) || $_[2];
		       return pack "N", 
		       (defined $self->{Dict}->vsattr_has_val($vid, $_[1])
			&& defined $self->{Dict}->vsaval_num($vid, @_[1, 0]) 
			) ?  $self->{Dict}->vsaval_num($vid, @_[1, 0]) : $_[0];
		   },
		   "ipaddr" => sub {
		       return inet_aton($_[0]);
		   },
		   "time" => sub {
		       return pack "N", $_[0];
		   },
		   "date" => sub {
		       return pack "N", $_[0];
		   },
		   "tagged-string" => sub { 
		       return $_[0]; 
		   },
		   "tagged-integer" => sub {
		       return $_[0];
		   },
		   "tagged-ipaddr" => sub {
		       my ($tag,$val)=unpack "C a*",$_[0];
		       return pack "C a*" , $tag , inet_aton($val);
		   });
    
  # Pack the attributes
  for (my $i = 0; $i < $self->attr_slots; ++$i)
  {
      my $attr = $self->attr_slot_name($i);
      if (! defined $self->{Dict}->attr_num($attr))
      {
	  carp("Unknown RADIUS tuple $attr => " . $self->attr_slot_val($i) 
	       . "\n")
	      if ($self->{unknown_entries});
	  next;
      }
      
      next unless ref($packer{$self->{Dict}->attr_type($attr)}) eq 'CODE';

      my $val = &{$packer{$self->{Dict}->attr_type($attr)}}
      ($self->attr_slot_val($i), $self->{Dict} ->attr_num($attr));

      $attstr .= pack $p_attr, $self->{Dict}->attr_num($attr),
      length($val)+2, $val;
  }

  # Pack the Vendor-Specific Attributes

  foreach my $vendor ($self->vendors) 
  {
      my $vid = $self->{Dict}->vendor_num($vendor) || $vendor;
      foreach my $attr ($self->vsattributes($vendor)) {
	next unless ref($vsapacker{$self->{Dict}
				   ->vsattr_type($vid, $attr)}) 
            eq 'CODE';
      foreach my $datum (@{$self->vsattr($vendor, $attr)}) {
        my $vval = &{$vsapacker{$self->{'Dict'}->vsattr_type($vid, $attr)}}
        ($datum, $self->{'Dict'}->vsattr_num($vid, $attr), $vendor);
Nigel Kukard's avatar
Nigel Kukard committed
        if ($vid == 429) {

      		# As pointed out by Quan Choi,
      		# we need special code to handle the
      		# 3Com case - See RFC-2882, sec 2.3.1

	    $attstr .= pack $p_vsa_3com, 26, 
	    length($vval) + 10, $vid,
	    $self->{'Dict'}->vsattr_num($vid, $attr),
	    $vval;
        } 
	else 
	{
	    $attstr .= pack $p_vsa, 26, length($vval) + 8, $vid,
	    $self->{'Dict'}->vsattr_num($vid, $attr),
	    length($vval) + 2, $vval;
        }
      }
    }
  }

  # Prepend the header and return the complete binary packet
  return pack $p_hdr, $codes{$self->code}, $self->identifier,
  length($attstr) + $hdrlen, $self->authenticator,
  $attstr;
}

sub unpack {
  my ($self, $data) = @_;
  my $dict = $self->{Dict};
  my $p_hdr  = "C C n a16 a*";    # Pack template for header
  my $p_attr = "C C a*";          # Pack template for attribute
  my $p_taggedattr = "C C C a*";  # Pack template for tagged-attribute
  my %rcodes = $dict->packet_names();

  # Decode the header
  my ($code, $id, $len, $auth, $attrdat) = unpack $p_hdr, $data;

  # Generate a skeleton data structure to be filled in
  $self->set_code($rcodes{$code});
  $self->set_identifier($id);
  $self->set_authenticator($auth);

  # Functions for the various data types
  my %unpacker = 
	(
	 "string" => sub {
	     return $_[0];
	 },
	 "ipv6addr" => sub { return $_[0]; },
	 "date" => sub { return $_[0]; },
	 "ifid" => sub { return $_[0]; },
	 "octets" => sub {
	     return $_[0];
	 },
	 "integer" => sub {
	     my $num=unpack("N", $_[0]);
	     return ( defined $dict->val_has_name($_[1]) &&
		      defined $dict->val_name($_[1],$num) ) ?
		      ($dict->val_name($_[1],$num),undef,$num) : $num ;
Nigel Kukard's avatar
Nigel Kukard committed
	 },
	 "ipaddr" => sub {
	     return length($_[0]) == 4 ? inet_ntoa($_[0]) : $_[0];
	 },
	 "address" => sub {
	     return length($_[0]) == 4 ? inet_ntoa($_[0]) : $_[0];
	 },
	 "time" => sub {
	     return unpack "N", $_[0];
	 },
	 "date" => sub {
	     return unpack "N", $_[0];
	 },
	 "tagged-string" => sub { 
	     my ($tag,$val) = unpack "a a*", $_[0]; 
	     return $val, $tag;
	 },
	 "tagged-integer" => sub {
	     my ($tag,$num) = unpack "a a*", $_[0];
	     return ( defined $dict->val_has_name($_[1]) &&
		      defined $dict->val_name($_[1],$num) ) ?
		      $dict->val_name($_[1],$num) : $num
		      ,$tag ;
	 },
	 "tagged-ipaddr" => sub {
	     my ( $tag, $num ) = unpack "a a*", $_[0];
	     return inet_ntoa($num), $tag;
	 });

  my %vsaunpacker = 
      ( 
	"octets" => sub {
	    return $_[0];
	},
	"string" => sub {
	    return $_[0];
	},
	"ipv6addr" => sub { return $_[0]; },
	"date" => sub { return $_[0]; },
	"ifid" => sub { return $_[0]; },
	"integer" => sub {
	    my $num=unpack("N", $_[0]);
	    return ( $dict->vsaval_has_name($_[2], $_[1]) 
		     && $dict->vsaval_name($_[2], $_[1],$num) )  
		? ( $dict->vsaval_name($_[2], $_[1], $num ), undef, $num)
Nigel Kukard's avatar
Nigel Kukard committed
		: $num;
	},
	"ipaddr" => sub {
	    return length($_[0]) == 4 ? inet_ntoa($_[0]) : $_[0];
	},
	"address" => sub {
	    return length($_[0]) == 4 ? inet_ntoa($_[0]) : $_[0];
	},
	"time" => sub {
	    return unpack "N", $_[0];
	},
	"date" => sub {
	    return unpack "N", $_[0];
	},
	"tagged-string" => sub { 
	    my ($tag,$val) = unpack "a a*", $_[0]; 
	    return $val, $tag;
	},
	"tagged-integer" => sub {
	    my ( $tag, $num ) = unpack "a a*", $_[0];
	    return  ($dict->vsaval_has_name($_[2], $_[1]) 
		     && $dict->vsaval_name($_[2], $_[1],$num) 
		     )?$dict->vsaval_name($_[2], $_[1],$num):$num 
		     , $tag ;
	    
	},
	"tagged-ipaddr" => sub {
	    my ( $tag, $num ) = unpack "a a*", $_[0];
	    return inet_ntoa($num), $tag;
	});
  
  # Unpack the attributes
  while (length($attrdat)) 
  {
      my $length = unpack "x C", $attrdat;
      my ($type, $value) = unpack "C x a${\($length-2)}", $attrdat;
      if ($type == $VSA) {    # Vendor-Specific Attribute
	  my ($vid) = unpack "N", $value;
	  substr ($value, 0, 4) = "";
	  
	  while (length($value))
	  {
	      my ($vtype, $vlength) = unpack "C C", $value;
	      
	      # XXX - How do we calculate the length
	      # of the VSA? It's not defined!
	      
	      # XXX - 3COM seems to do things a bit differently. 
	      # The IF below takes care of that. This was contributed by 
	      # Ian Smith. Check the file CHANGES on this distribution for 
	      # more information.

	      my $vvalue;
	      if ($vid == 429)
	      {
		  ($vtype) = unpack "N", $value;
		  $vvalue = unpack "xxxx a${\($length-10)}", $value;
	      }
	      else
	      {
		  $vvalue = unpack "x x a${\($vlength-2)}", $value;
	      }

	      if ((not defined $dict->vsattr_numtype($vid, $vtype)) or 
		  (ref $vsaunpacker{$dict->vsattr_numtype($vid, $vtype)} 
		   ne 'CODE')) 
	      {
		  my $whicherr 
		      = (defined $dict->vsattr_numtype($vid, $vtype)) ?
		      "Garbled":"Unknown";
		  warn "$whicherr vendor attribute $vid/$vtype for unpack()\n"
		      unless $unkvprinted{"$vid/$vtype"};
		  $unkvprinted{"$vid/$vtype"} = 1;
		  substr($value, 0, $vlength) = ""; # Skip this section
		  next;
	      }
	      my ($val, $tag, $rawValue) = 
Nigel Kukard's avatar
Nigel Kukard committed
		  &{$vsaunpacker{$dict->vsattr_numtype($vid, $vtype)}}($vvalue,
								       $vtype,
								       $vid);
	      if ( defined $tag ) 
	      {
		  $val = "-emtpy-" unless defined $val;
		  $self->set_taggedvsattr($vid,
					  $dict->vsattr_name($vid, $vtype),
					  $val, 
					  $tag);
	      }
	      else 
	      {
		  $self->set_vsattr($vid, $dict->vsattr_name($vid, $vtype), 
				    $val, undef, $rawValue);
Nigel Kukard's avatar
Nigel Kukard committed
	      }
	      substr($value, 0, $vlength) = "";
	  }
      }
      else 
      {            # Normal attribute
	  if ((not defined $dict->attr_numtype($type)) or
	      (ref ($unpacker{$dict->attr_numtype($type)}) ne 'CODE')) 
	  {
	      my $whicherr = (defined $dict->attr_numtype($type)) ?
		  "Garbled":"Unknown";
	      warn "$whicherr general attribute $type for unpack()\n"
		  unless $unkgprinted{$type};
	      $unkgprinted{$type} = 1;
	      substr($attrdat, 0, $length) = ""; # Skip this section
	      next;
	  }
	  my ($val,$tag,$rawValue) = &{$unpacker{$dict->attr_numtype($type)}}($value, 
Nigel Kukard's avatar
Nigel Kukard committed
								    $type);
	  if ( defined $tag ) {
	      if ( ! defined $val ) { $val = "-emtpy-" };
	      $self->set_taggedattr($dict->attr_name($type), $val , $tag);
	  }
	  else {
	      $self->set_attr($dict->attr_name($type), $val, undef, $rawValue);
Nigel Kukard's avatar
Nigel Kukard committed
	  }
      }
      substr($attrdat, 0, $length) = ""; # Skip this section
  }
}

1;