File Coverage

blib/lib/Data/Radius/Encode.pm
Criterion Covered Total %
statement 105 170 61.7
branch 49 92 53.2
condition 13 40 32.5
subroutine 24 28 85.7
pod 0 19 0.0
total 191 349 54.7


line stmt bran cond sub pod time code
1             package Data::Radius::Encode;
2              
3 5     5   105639 use strict;
  5         8  
  5         182  
4 5     5   22 use warnings;
  5         7  
  5         233  
5 5     5   25 use Carp ();
  5         10  
  5         177  
6 5     5   338 use bytes;
  5         447  
  5         22  
7 5     5   2950 use Socket qw(inet_pton AF_INET AF_INET6);
  5         24624  
  5         1267  
8              
9             use constant {
10 5         605 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 5     5   46 };
  5         10  
16              
17 5     5   36 use Exporter qw(import);
  5         11  
  5         316  
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 5     5   2429 use Data::Radius::Util qw(is_enum_type);
  5         20  
  5         15447  
36              
37             our ($PrintError, $RaiseError) = (1, 0);
38              
39             sub _error {
40 15     15   35 my $msg = shift;
41 15 50       56 Carp::croak($msg) if $RaiseError;
42 15 50       2430 Carp::carp ($msg) if $PrintError;
43 15         10075 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             ipv4prefix => \&encode_ipv4prefix,
59             ipv6prefix => \&encode_ipv6prefix,
60             avpair => \&encode_avpair,
61             'combo-ip' => \&encode_combo_ip,
62             octets => \&encode_octets,
63             tlv => \&encode_tlv,
64             # Unix timestamp
65             date => \&encode_int,
66             #TODO Ascend binary encoding
67             # abinary => ...
68             );
69              
70             if (!defined inet_pton(AF_INET6, '::1')) {
71             require Net::IP;
72             $encode_map{ipv6addr} = \&encode_ipv6addr_pp,
73             $encode_map{ipv6prefix} = \&encode_ipv6prefix_pp,
74             }
75              
76             sub encode_string {
77 17     17 0 68 my ($value, $attr, $dict) = @_;
78 17 100 66     127 my $max_size = ($attr && $attr->{vendor}) ? MAX_VSA_STRING_SIZE : MAX_STRING_SIZE;
79 17 100       71 if ( length($value) > $max_size) {
80 2         38 _error( "Too long value for attribute '$attr->{name}'" );
81 2         6 $value = undef; # substr($value, $max_size); # forgiving option?
82             }
83 17         47 return $value;
84             }
85              
86             sub encode_string_tag {
87 1     1 0 5 my ($value, $attr, $dict, $tag) = @_;
88              
89 1 50 33     18 if (! defined $tag ) {
    50          
    50          
    50          
90 0         0 _error( "Undefined tag value for attribute '$attr->{name}'");
91             }
92             elsif ( $tag !~ /^\d+$/ ) {
93 0         0 _error( "Invalid tag value '$tag' for attribute '$attr->{name}'" );
94             }
95             elsif ( $tag == 0 ) {
96             # it should be possible to correctly indicate to not to utilize tag
97             }
98             elsif ($tag < 1 || $tag > 31) {
99 0         0 _error( "Tag value $tag out of range 1..31 for attribute '$attr->{name}'" );
100             }
101             else {
102 1         5 $value = pack('C', $tag) . $value;
103             }
104              
105 1 50 33     7 my $max_size = ($attr && $attr->{vendor}) ? MAX_VSA_STRING_SIZE : MAX_STRING_SIZE;
106 1 50       4 if ( length($value) > $max_size) {
107 0         0 _error( "Too long value for attribute '$attr->{name}'" );
108 0         0 $value = undef; # substr($value, $max_size); # forgiving option?
109             }
110              
111 1         4 return $value;
112             }
113              
114             sub check_numeric {
115 20     20 0 53 my ($value, $attr, $range) = @_;
116 20 100       148 if ($value !~ /^-?\d+$/) {
117 1         12 _error( "Invalid value for numeric attribute '$attr->{name}'" );
118 1         6 return;
119             }
120 19 50       55 if ($range) {
121 19 100 100     131 if ($value < $range->[0] || $value > $range->[1]) {
122 8         54 _error( "Value out of range for $attr->{type} attribute '$attr->{name}'" );
123 8         41 return undef;
124             }
125             }
126 11         93 return 1;
127             }
128              
129 7 100   7 0 32 sub encode_int { return check_numeric($_[0], $_[1], [0, 2**32 - 1]) ? pack('N', int($_[0])) : undef }
130 4 100   4 0 17 sub encode_byte { return check_numeric($_[0], $_[1], [0, 2**8 - 1]) ? pack('C', int($_[0])) : undef }
131 3 100   3 0 11 sub encode_short { return check_numeric($_[0], $_[1], [0, 2**16 - 1]) ? pack('S>', int($_[0])) : undef }
132 4 100   4 0 16 sub encode_signed { return check_numeric($_[0], $_[1], [-2**31, 2**31 - 1]) ? pack('l>', int($_[0])) : undef }
133              
134             sub encode_int_tag {
135 2     2 0 7 my ($value, $attr, $dict, $tag) = @_;
136 2 50       8 return undef if !check_numeric($value, $attr, [0, 2**24 - 1]);
137 2         10 $value = pack('N', int($value));
138 2 50 33     21 if (! defined $tag ) {
    50          
    50          
    50          
139 0         0 _error( "Undefined tag value for attribute '$attr->{name}'");
140             }
141             elsif ( $tag !~ /^\d+$/ ) {
142 0         0 _error( "Invalid tag value '$tag' for attribute '$attr->{name}'" );
143             }
144             elsif ( $tag == 0 ) {
145             # it should be possible to correctly indicate to not to utilize tag
146             }
147             elsif ($tag < 1 || $tag > 31) {
148 0         0 _error( "Tag value $tag out of range 1..31 for attribute '$attr->{name}'" );
149             }
150             else {
151             # tag added to 1st byte, not extending the value length
152 2         8 substr($value, 0, 1, pack('C', $tag) );
153             }
154 2         6 return $value;
155             }
156              
157 3     3 0 18 sub encode_ipaddr { inet_pton(AF_INET, $_[0]) }
158 3     3 0 18 sub encode_ipv6addr { inet_pton(AF_INET6, $_[0]) }
159              
160             sub encode_ipv6addr_pp {
161 0     0 0 0 my $value = shift;
162 0         0 my $expanded_value = Net::IP::ip_expand_address( $value, 6 );
163 0 0       0 return undef if (! $expanded_value);
164 0         0 my $bin_value = Net::IP::ip_iptobin( $expanded_value, 6 );
165 0 0       0 return undef if (! defined $bin_value);
166 0         0 return pack( 'B*', $bin_value );
167             }
168              
169             sub encode_octets {
170 2     2 0 6 my ($value, $attr, $dict) = @_;
171              
172 2 100       17 if ($value !~ /^0x(?:[0-9A-Fa-f]{2})+$/) {
173 1         7 _error( "Invalid octet string value for attribute '$attr->{name}'" );
174 1         4 return undef;
175             }
176              
177 1         5 $value =~ s/^0x//;
178 1         7 return pack("H*", $value);
179             }
180              
181             sub encode_combo_ip {
182 2     2 0 5 my $ip = shift;
183              
184 2 100       40 if ($ip =~ /^\d+\.\d+.\d+.\d+$/) {
185 1         6 return $encode_map{ipaddr}->($ip);
186             }
187              
188 1         6 return $encode_map{ipv6addr}->($ip);
189             }
190              
191             sub encode_avpair {
192 4     4 0 11 my ($value, $attr, $dict) = @_;
193 4 50 50     23 if ( ($attr->{vendor} // '') eq VENDOR_CISCO ) {
194             # Looks like it afects only requests from Cisco NAS
195             # and probably not required in requests to it
196             # Do not applied to Cisco-AVPair attribute itself
197 4 100 66     24 if ($attr->{id} == ATTR_CISCO_AVPAIR_ID && $attr->{name} ne ATTR_CISCO_AVPAIR) {
198 2         7 $value = $attr->{name} . '=' . $value;
199             }
200             }
201              
202 4 100       13 if (length($value) > MAX_VSA_STRING_SIZE) {
203 2         10 _error( "Too long value for attribute '$attr->{name}'" );
204 2         9 return undef;
205             }
206              
207 2         8 return $value;
208             }
209              
210             # TODO continuation field is not supported for WiMAX VSA
211             sub encode_tlv {
212 2     2 0 5 my ($value, $parent, $dict) = @_;
213              
214 2         5 my @list = ();
215 2         5 foreach my $v (@{$value}) {
  2         5  
216 3         14 my $attr = $dict->attribute($v->{Name});
217 3 50       28 if (! $attr) {
218 0         0 _error( "Unknown tlv-attribute '$v->{Name}' for attribute '$parent->{name}'" );
219 0         0 next;
220             }
221              
222             # no vendor for sub-attributes
223              
224             # verify that corrent sub-attribute is used
225 3 50 50     16 if ( ($attr->{parent} // '') ne $parent->{name}) {
226 0         0 _error( "Attribute '$v->{Name}' is not a tlv of attribute '$parent->{name}'" );
227 0         0 next;
228             }
229              
230             # constant to its value
231 3         5 my $value;
232 3 100       15 if (is_enum_type($attr->{type})) {
233 1   33     7 $value = $dict->value($attr->{name}, $v->{Value}) // $v->{Value};
234             }
235             else {
236 2         6 $value = $v->{Value};
237             }
238              
239 3         48 my $encoded = encode($attr, $value, $dict);
240              
241 3         20 push @list, pack('C C', $attr->{id}, length($encoded) + 2) . $encoded;
242             }
243              
244 2         11 return join('', @list);
245             }
246              
247             sub encode_ipv4prefix {
248 0     0 0 0 my ($value, $attr) = @_;
249              
250 0         0 my ($ip, $prefix_len);
251 0 0       0 if ($value =~ /^(\d+\.\d+\.\d+\.\d+)(?:\/(\d+))?$/) {
252 0   0     0 ($ip, $prefix_len) = ($1, $2 || 0);
253             }
254             else {
255 0         0 _error("Invalid IPv4 prefix format for attribute '$attr->{name}'. Expected format: ip/prefix-length or just ip");
256 0         0 return undef;
257             }
258              
259 0 0 0     0 if ($prefix_len < 0 || $prefix_len > 32) {
260 0         0 _error("Invalid prefix length for IPv4 prefix in attribute '$attr->{name}'. Must be between 0 and 32");
261 0         0 return undef;
262             }
263              
264 0         0 my $ip_bin = inet_pton(AF_INET, $ip);
265 0 0       0 unless (defined $ip_bin) {
266 0         0 _error("Invalid IPv4 address format for attribute '$attr->{name}': $ip");
267 0         0 return undef;
268             }
269              
270 0         0 return pack('Ca*', $prefix_len, $ip_bin);
271             }
272              
273             sub encode_ipv6prefix {
274 0     0 0 0 my ($value, $attr) = @_;
275              
276 0         0 my ($ip, $prefix_len);
277 0 0       0 if ($value =~ /^([0-9a-fA-F:]+)(?:\/(\d+))?$/) {
278 0   0     0 ($ip, $prefix_len) = ($1, $2 || 0);
279             }
280             else {
281 0         0 _error("Invalid IPv6 prefix format for attribute '$attr->{name}'. Expected format: ipv6/prefix-length or just ipv6");
282 0         0 return undef;
283             }
284              
285 0 0 0     0 if ($prefix_len < 0 || $prefix_len > 128) {
286 0         0 _error("Invalid prefix length for IPv6 prefix in attribute '$attr->{name}'. Must be between 0 and 128");
287 0         0 return undef;
288             }
289              
290 0         0 my $ip_bin = inet_pton(AF_INET6, $ip);
291 0 0       0 unless (defined $ip_bin) {
292 0         0 _error("Invalid IPv6 address format for attribute '$attr->{name}': $ip");
293 0         0 return undef;
294             }
295              
296 0         0 return pack('Ca*', $prefix_len, $ip_bin);
297             }
298              
299             sub encode_ipv6prefix_pp {
300 0     0 0 0 my ($value, $attr) = @_;
301              
302 0         0 my ($ip, $prefix_len);
303 0 0       0 if ($value =~ /^([0-9a-fA-F:]+)(?:\/(\d+))?$/) {
304 0   0     0 ($ip, $prefix_len) = ($1, $2 || 0);
305             }
306             else {
307 0         0 _error("Invalid IPv6 prefix format for attribute '$attr->{name}'. Expected format: ipv6/prefix-length or just ipv6");
308 0         0 return undef;
309             }
310              
311 0 0 0     0 if ($prefix_len < 0 || $prefix_len > 128) {
312 0         0 _error("Invalid prefix length for IPv6 prefix in attribute '$attr->{name}'. Must be between 0 and 128");
313 0         0 return undef;
314             }
315              
316 0         0 my $expanded_value = Net::IP::ip_expand_address($ip, 6);
317 0 0       0 unless ($expanded_value) {
318 0         0 _error("Invalid IPv6 address format for attribute '$attr->{name}': $ip");
319 0         0 return undef;
320             }
321              
322 0         0 my $bin_value = Net::IP::ip_iptobin($expanded_value, 6);
323 0 0       0 unless (defined $bin_value) {
324 0         0 _error("Failed to convert IPv6 address to binary for attribute '$attr->{name}': $ip");
325 0         0 return undef;
326             }
327              
328 0         0 return pack('CB*', $prefix_len, $bin_value);
329             }
330              
331             # main exported function
332             sub encode {
333 53     53 0 5266 my ($attr, $value, $dict, $tag) = @_;
334              
335 53 100       166 if (! defined $value) {
336 1         5 _error( "Undefined value for attribute '$attr->{name}'" );
337 1         6 return undef;
338             }
339              
340 52         100 my ($encoder_type, $encoder_sub, $encoded);
341              
342 52 100       150 if ($attr->{has_tag}) {
343 3         9 $encoder_type .= $attr->{type}.'_tag';
344             }
345             else {
346 49         129 $encoder_type = $attr->{type};
347 49 50       190 _error( "Provided Tag for tagless attribute '$attr->{name}'") if defined $tag;
348             }
349              
350 52 50       196 if ($encoder_sub = $encode_map{ $encoder_type }) {
351 52         159 $encoded = $encoder_sub->($value, $attr, $dict, $tag);
352             }
353             else {
354 0         0 _error( "Unsupported encoding type '$encoder_type' for attribute '$attr->{name}'" );
355             }
356              
357 52         274 return $encoded;
358             }
359              
360             1;