File Coverage

blib/lib/Data/Radius/Decode.pm
Criterion Covered Total %
statement 67 85 78.8
branch 14 26 53.8
condition 2 5 40.0
subroutine 22 25 88.0
pod 0 17 0.0
total 105 158 66.4


line stmt bran cond sub pod time code
1             package Data::Radius::Decode;
2              
3 5     5   148179 use v5.10;
  5         22  
4 5     5   30 use strict;
  5         8  
  5         164  
5 5     5   24 use warnings;
  5         10  
  5         346  
6 5     5   633 use bytes;
  5         656  
  5         72  
7 5     5   855 use Socket qw(inet_ntop inet_pton AF_INET AF_INET6);
  5         7631  
  5         628  
8              
9             use constant {
10 5         598 ATTR_CISCO_AVPAIR => 1,
11             VENDOR_CISCO => 'Cisco',
12 5     5   33 };
  5         9  
13              
14 5     5   47 use Exporter qw(import);
  5         130  
  5         443  
15             our @EXPORT_OK = qw(
16             decode
17              
18             decode_string
19             decode_int
20             decode_byte
21             decode_short
22             decode_signed
23             decode_ipaddr
24             decode_ipv6addr
25             decode_combo_ip
26             decode_octets
27             decode_avpair
28             decode_tlv
29             );
30              
31 5     5   613 use Data::Radius::Util qw(is_enum_type);
  5         13  
  5         6342  
32              
33             # type decoders
34             # $coderef->($value, $attr, $dictionary)
35             my %decode_map = (
36             string => \&decode_string,
37             string_tag => \&decode_string_tag,
38             integer => \&decode_int,
39             integer_tag => \&decode_int_tag,
40             byte => \&decode_byte,
41             short => \&decode_short,
42             signed => \&decode_signed,
43             ipaddr => \&decode_ipaddr,
44             ipv6addr => \&decode_ipv6addr,
45             ipv4prefix => \&decode_ipv4prefix,
46             ipv6prefix => \&decode_ipv6prefix,
47             avpair => \&decode_avpair,
48             'combo-ip' => \&decode_combo_ip,
49             octets => \&decode_octets,
50             tlv => \&decode_tlv,
51             # Unix timestamp
52             date => \&decode_int,
53             #TODO Ascend binary encoding
54             # abinary => ...
55             );
56              
57             if (!defined inet_pton(AF_INET6, '::1')) {
58             require Net::IP;
59             $decode_map{ipv6addr} = \&decode_ipv6addr_pp;
60             }
61              
62 11     11 0 22 sub decode_string { $_[0] }
63              
64             sub decode_string_tag {
65 1     1 0 3 my $value = shift;
66             # https://tools.ietf.org/html/rfc2868#section-3.3
67             # If the Tag field is greater than 0x1F, it SHOULD be
68             # interpreted as the first byte of the following String field
69 1 50       5 return if (length($value) < 1);
70              
71 1         4 my $tag = unpack('C', substr($value, 0, 1));
72 1 50       21 if ($tag > 0x1F) {
73 0         0 return ($value, undef);
74             }
75 1         5 return (substr($value, 1), $tag);
76             }
77              
78 3     3 0 13 sub decode_int { unpack('N', $_[0]) }
79 2     2 0 6 sub decode_byte { unpack('C', $_[0]) }
80 1     1 0 4 sub decode_short { unpack('S>', $_[0]) }
81 1     1 0 4 sub decode_signed { unpack('l>', $_[0]) }
82              
83             sub decode_int_tag {
84 2     2 0 6 my $value = shift;
85             # https://tools.ietf.org/html/rfc6158#section-3.2.2
86             # when integer values are tagged, the value portion is reduced to three bytes
87              
88             # replace tag by 0 to make unpack() value work
89 2         9 my $tag = unpack('C', substr($value, 0, 1, "\x00"));
90 2         8 return (unpack('N', $value), $tag);
91             }
92              
93 2     2 0 17 sub decode_ipaddr { inet_ntop(AF_INET, $_[0]) }
94 2     2 0 11 sub decode_ipv6addr { inet_ntop(AF_INET6, $_[0]) }
95              
96             sub decode_ipv6addr_pp {
97 0     0 0 0 my $value = shift;
98              
99 0         0 my $binary = unpack( 'B*', $value );
100 0 0       0 return undef if (! $binary);
101 0         0 my $ip_val = Net::IP::ip_bintoip( $binary, 6 );
102 0 0       0 return undef if (! $ip_val);
103 0         0 return Net::IP::ip_compress_address( $ip_val, 6 );
104             }
105              
106 1     1 0 6 sub decode_octets { '0x'.unpack("H*", $_[0]) }
107              
108             sub decode_combo_ip {
109 2     2 0 4 my $ip = shift;
110              
111 2 100       5 if (length($ip) == 4) {
112 1         3 return $decode_map{ipaddr}->($ip);
113             }
114 1         3 return $decode_map{ipv6addr}->($ip);
115             }
116              
117             sub decode_avpair {
118 3     3 0 4 my ($value, $attr, $dict) = @_;
119 3 50 50     11 if ( ($attr->{vendor} // '') eq VENDOR_CISCO) {
120             # Cisco hack
121 3 100       7 if ( $attr->{id} == ATTR_CISCO_AVPAIR ) {
122             # Cisco-AVPair = "h323-foo-bar=baz"
123             # leave it as-is
124             }
125             else {
126             # h323-foo-bar = "h323-foo-bar = baz"
127             # cut attribute name
128 2         41 $value =~ s/^\Q$attr->{name}\E\s*=//;
129             }
130             }
131              
132 3         7 return $value;
133             }
134              
135             sub decode_tlv {
136 2     2 0 4 my ($value, $parent, $dict) = @_;
137              
138 2         4 my $pos = 0;
139 2         2 my $len = length($value);
140              
141 2         3 my @list = ();
142 2         6 while ($pos < $len) {
143 3         9 my ($attr_id, $attr_len) = unpack('C C', substr($value, $pos, 2));
144 3         7 my $attr_val = substr($value, $pos + 2, $attr_len - 2);
145              
146 3         9 my $attr = $dict->tlv_attribute_name($parent, $attr_id);
147 3 50       6 if (! $attr) {
148 0         0 push @list, {Name => $attr_id, Value => $attr_val, Unknown => 1};
149             }
150             else {
151 3         5 my $decoded = decode($attr, $attr_val, $dict);
152 3 100       7 if (is_enum_type($attr->{type})) {
153 1   33     5 $decoded = $dict->constant($attr->{name}, $decoded) // $decoded;
154             }
155              
156 3         32 push @list, {Name => $attr->{name}, Value => $decoded, Type => $attr->{type}};
157             }
158              
159 3         6 $pos += $attr_len;
160             }
161              
162 2         5 return \@list;
163             }
164              
165             sub decode_ipv4prefix {
166 0     0 0 0 my $value = shift;
167             # Format:
168             # prefix-length is 1 byte, ipv4-address is 4 bytes
169 0 0       0 return undef if length($value) != 5;
170              
171 0         0 my ( $prefix_len, $ip ) = unpack('Ca*', $value);
172 0         0 my $ip_str = inet_ntop(AF_INET, $ip);
173              
174 0         0 return "$ip_str/$prefix_len";
175             }
176              
177             sub decode_ipv6prefix {
178 0     0 0 0 my $value = shift;
179             # Format:
180             # prefix-length is 1 byte, ipv6-address is 16 bytes
181 0 0       0 return undef if length($value) != 17;
182              
183 0         0 my ( $prefix_len, $ip ) = unpack('Ca*', $value);
184 0         0 my $ip_str = inet_ntop(AF_INET6, $ip);
185              
186 0         0 return "$ip_str/$prefix_len";
187             }
188              
189             sub decode {
190 31     31 0 3241 my ($attr, $value, $dict) = @_;
191              
192 31 100       97 my $decoder = $attr->{type} . ($attr->{has_tag} ? '_tag' : '');
193 31         95 my ($decoded, $tag) = $decode_map{ $decoder }->($value, $attr, $dict);
194 31 100       132 return wantarray ? ($decoded, $tag) : $decoded;
195             }
196              
197             1;