File Coverage

blib/lib/Data/Radius/Encode.pm
Criterion Covered Total %
statement 105 124 84.6
branch 49 72 68.0
condition 13 25 52.0
subroutine 24 25 96.0
pod 0 16 0.0
total 191 262 72.9


line stmt bran cond sub pod time code
1             package Data::Radius::Encode;
2              
3 4     4   64301 use strict;
  4         14  
  4         98  
4 4     4   15 use warnings;
  4         7  
  4         84  
5 4     4   14 use Carp ();
  4         7  
  4         57  
6 4     4   613 use bytes;
  4         17  
  4         15  
7 4     4   1907 use Socket qw(inet_pton AF_INET AF_INET6);
  4         12989  
  4         744  
8              
9             use constant {
10 4         366 MAX_STRING_SIZE => 253,
11             MAX_VSA_STRING_SIZE => 247,
12             ATTR_CISCO_AVPAIR => 'Cisco-AVPair',
13             ATTR_CISCO_AVPAIR_ID => 1,
14             VENDOR_CISCO => 'Cisco',
15 4     4   36 };
  4         6  
16              
17 4     4   22 use Exporter qw(import);
  4         6  
  4         180  
18              
19             our @EXPORT_OK = qw(
20             encode
21              
22             encode_string
23             encode_int
24             encode_byte
25             encode_short
26             encode_signed
27             encode_ipaddr
28             encode_ipv6addr
29             encode_combo_ip
30             encode_octets
31             encode_avpair
32             encode_tlv
33             );
34              
35 4     4   1492 use Data::Radius::Util qw(is_enum_type);
  4         8  
  4         6347  
36              
37             our ($PrintError, $RaiseError) = (1, 0);
38              
39             sub _error {
40 15     15   20 my $msg = shift;
41 15 50       32 Carp::croak($msg) if $RaiseError;
42 15 50       1554 Carp::carp ($msg) if $PrintError;
43 15         4540 return;
44             }
45              
46             # type encoders
47             # $coderef->($value, $attr, $dictionary)
48             my %encode_map = (
49             string => \&encode_string,
50             string_tag => \&encode_string_tag,
51             integer => \&encode_int,
52             integer_tag => \&encode_int_tag,
53             byte => \&encode_byte,
54             short => \&encode_short,
55             signed => \&encode_signed,
56             ipaddr => \&encode_ipaddr,
57             ipv6addr => \&encode_ipv6addr,
58             avpair => \&encode_avpair,
59             'combo-ip' => \&encode_combo_ip,
60             octets => \&encode_octets,
61             tlv => \&encode_tlv,
62             # Unix timestamp
63             date => \&encode_int,
64             #TODO Ascend binary encoding
65             # abinary => ...
66             );
67              
68             if (!defined inet_pton(AF_INET6, '::1')) {
69             require Net::IP;
70             $encode_map{ipv6addr} = \&encode_ipv6addr_pp,
71             }
72              
73             sub encode_string {
74 16     16 0 33 my ($value, $attr, $dict) = @_;
75 16 100 66     76 my $max_size = ($attr && $attr->{vendor}) ? MAX_VSA_STRING_SIZE : MAX_STRING_SIZE;
76 16 100       38 if ( length($value) > $max_size) {
77 2         8 _error( "Too long value for attribute '$attr->{name}'" );
78 2         5 $value = undef; # substr($value, $max_size); # forgiving option?
79             }
80 16         30 return $value;
81             }
82              
83             sub encode_string_tag {
84 1     1 0 3 my ($value, $attr, $dict, $tag) = @_;
85              
86 1 50 33     22 if (! defined $tag ) {
    50          
    50          
    50          
87 0         0 _error( "Undefined tag value for attribute '$attr->{name}'");
88             }
89             elsif ( $tag !~ /^\d+$/ ) {
90 0         0 _error( "Invalid tag value '$tag' for attribute '$attr->{name}'" );
91             }
92             elsif ( $tag == 0 ) {
93             # it should be possible to correctly indicate to not to utilize tag
94             }
95             elsif ($tag < 1 || $tag > 31) {
96 0         0 _error( "Tag value $tag out of range 1..31 for attribute '$attr->{name}'" );
97             }
98             else {
99 1         5 $value = pack('C', $tag) . $value;
100             }
101              
102 1 50 33     6 my $max_size = ($attr && $attr->{vendor}) ? MAX_VSA_STRING_SIZE : MAX_STRING_SIZE;
103 1 50       4 if ( length($value) > $max_size) {
104 0         0 _error( "Too long value for attribute '$attr->{name}'" );
105 0         0 $value = undef; # substr($value, $max_size); # forgiving option?
106             }
107              
108 1         4 return $value;
109             }
110              
111             sub check_numeric {
112 18     18 0 35 my ($value, $attr, $range) = @_;
113 18 100       94 if ($value !~ /^-?\d+$/) {
114 1         6 _error( "Invalid value for numeric attribute '$attr->{name}'" );
115 1         4 return;
116             }
117 17 50       37 if ($range) {
118 17 100 100     79 if ($value < $range->[0] || $value > $range->[1]) {
119 8         35 _error( "Value out of range for $attr->{type} attribute '$attr->{name}'" );
120 8         29 return undef;
121             }
122             }
123 9         60 return 1;
124             }
125              
126 5 100   5 0 17 sub encode_int { return check_numeric($_[0], $_[1], [0, 2**32 - 1]) ? pack('N', int($_[0])) : undef }
127 4 100   4 0 15 sub encode_byte { return check_numeric($_[0], $_[1], [0, 2**8 - 1]) ? pack('C', int($_[0])) : undef }
128 3 100   3 0 8 sub encode_short { return check_numeric($_[0], $_[1], [0, 2**16 - 1]) ? pack('S>', int($_[0])) : undef }
129 4 100   4 0 11 sub encode_signed { return check_numeric($_[0], $_[1], [-2**31, 2**31 - 1]) ? pack('l>', int($_[0])) : undef }
130              
131             sub encode_int_tag {
132 2     2 0 4 my ($value, $attr, $dict, $tag) = @_;
133 2 50       6 return undef if !check_numeric($value, $attr, [0, 2**24 - 1]);
134 2         9 $value = pack('N', int($value));
135 2 50 33     22 if (! defined $tag ) {
    50          
    50          
    50          
136 0         0 _error( "Undefined tag value for attribute '$attr->{name}'");
137             }
138             elsif ( $tag !~ /^\d+$/ ) {
139 0         0 _error( "Invalid tag value '$tag' for attribute '$attr->{name}'" );
140             }
141             elsif ( $tag == 0 ) {
142             # it should be possible to correctly indicate to not to utilize tag
143             }
144             elsif ($tag < 1 || $tag > 31) {
145 0         0 _error( "Tag value $tag out of range 1..31 for attribute '$attr->{name}'" );
146             }
147             else {
148             # tag added to 1st byte, not extending the value length
149 2         6 substr($value, 0, 1, pack('C', $tag) );
150             }
151 2         4 return $value;
152             }
153              
154 3     3 0 16 sub encode_ipaddr { inet_pton(AF_INET, $_[0]) }
155 3     3 0 15 sub encode_ipv6addr { inet_pton(AF_INET6, $_[0]) }
156              
157             sub encode_ipv6addr_pp {
158 0     0 0 0 my $value = shift;
159 0         0 my $expanded_value = Net::IP::ip_expand_address( $value, 6 );
160 0 0       0 return undef if (! $expanded_value);
161 0         0 my $bin_value = Net::IP::ip_iptobin( $expanded_value, 6 );
162 0 0       0 return undef if (! defined $bin_value);
163 0         0 return pack( 'B*', $bin_value );
164             }
165              
166             sub encode_octets {
167 2     2 0 4 my ($value, $attr, $dict) = @_;
168              
169 2 100       13 if ($value !~ /^0x(?:[0-9A-Fa-f]{2})+$/) {
170 1         7 _error( "Invalid octet string value for attribute '$attr->{name}'" );
171 1         3 return undef;
172             }
173              
174 1         5 $value =~ s/^0x//;
175 1         9 return pack("H*", $value);
176             }
177              
178             sub encode_combo_ip {
179 2     2 0 4 my $ip = shift;
180              
181 2 100       12 if ($ip =~ /^\d+\.\d+.\d+.\d+$/) {
182 1         4 return $encode_map{ipaddr}->($ip);
183             }
184              
185 1         4 return $encode_map{ipv6addr}->($ip);
186             }
187              
188             sub encode_avpair {
189 4     4 0 11 my ($value, $attr, $dict) = @_;
190 4 50 50     14 if ( ($attr->{vendor} // '') eq VENDOR_CISCO ) {
191             # Looks like it afects only requests from Cisco NAS
192             # and probably not required in requests to it
193             # Do not applied to Cisco-AVPair attribute itself
194 4 100 66     19 if ($attr->{id} == ATTR_CISCO_AVPAIR_ID && $attr->{name} ne ATTR_CISCO_AVPAIR) {
195 2         7 $value = $attr->{name} . '=' . $value;
196             }
197             }
198              
199 4 100       10 if (length($value) > MAX_VSA_STRING_SIZE) {
200 2         9 _error( "Too long value for attribute '$attr->{name}'" );
201 2         5 return undef;
202             }
203              
204 2         5 return $value;
205             }
206              
207             # TODO continuation field is not supported for WiMAX VSA
208             sub encode_tlv {
209 2     2 0 4 my ($value, $parent, $dict) = @_;
210              
211 2         4 my @list = ();
212 2         4 foreach my $v (@{$value}) {
  2         4  
213 3         11 my $attr = $dict->attribute($v->{Name});
214 3 50       23 if (! $attr) {
215 0         0 _error( "Unknown tlv-attribute '$v->{Name}' for attribute '$parent->{name}'" );
216 0         0 next;
217             }
218              
219             # no vendor for sub-attributes
220              
221             # verify that corrent sub-attribute is used
222 3 50 50     11 if ( ($attr->{parent} // '') ne $parent->{name}) {
223 0         0 _error( "Attribute '$v->{Name}' is not a tlv of attribute '$parent->{name}'" );
224 0         0 next;
225             }
226              
227             # constant to its value
228 3         6 my $value;
229 3 100       9 if (is_enum_type($attr->{type})) {
230 1   33     7 $value = $dict->value($attr->{name}, $v->{Value}) // $v->{Value};
231             }
232             else {
233 2         5 $value = $v->{Value};
234             }
235              
236 3         33 my $encoded = encode($attr, $value, $dict);
237              
238 3         18 push @list, pack('C C', $attr->{id}, length($encoded) + 2) . $encoded;
239             }
240              
241 2         8 return join('', @list);
242             }
243              
244             # main exported function
245             sub encode {
246 50     50 0 2919 my ($attr, $value, $dict, $tag) = @_;
247              
248 50 100       106 if (! defined $value) {
249 1         5 _error( "Undefined value for attribute '$attr->{name}'" );
250 1         5 return undef;
251             }
252              
253 49         68 my ($encoder_type, $encoder_sub, $encoded);
254              
255 49 100       95 if ($attr->{has_tag}) {
256 3         7 $encoder_type .= $attr->{type}.'_tag';
257             }
258             else {
259 46         62 $encoder_type = $attr->{type};
260 46 50       95 _error( "Provided Tag for tagless attribute '$attr->{name}'") if defined $tag;
261             }
262              
263 49 50       102 if ($encoder_sub = $encode_map{ $encoder_type }) {
264 49         103 $encoded = $encoder_sub->($value, $attr, $dict, $tag);
265             }
266             else {
267 0         0 _error( "Unsupported encoding type '$encoder_type' for attribute '$attr->{name}'" );
268             }
269              
270 49         185 return $encoded;
271             }
272              
273             1;