File Coverage

blib/lib/Net/RNDC/Packet.pm
Criterion Covered Total %
statement 158 187 84.4
branch 41 58 70.6
condition 17 23 73.9
subroutine 31 35 88.5
pod 4 4 100.0
total 251 307 81.7


line stmt bran cond sub pod time code
1             package Net::RNDC::Packet;
2             {
3             $Net::RNDC::Packet::VERSION = '0.003';
4             }
5              
6 3     3   104791 use strict;
  3         7  
  3         133  
7 3     3   19 use warnings;
  3         107  
  3         226  
8              
9 3     3   2059 use Net::RNDC::Exception;
  3         7  
  3         103  
10              
11 3     3   3544 use Try::Tiny;
  3         25353  
  3         236  
12              
13 3     3   5848 use UNIVERSAL ();
  3         99  
  3         99  
14              
15 3     3   18 use Carp qw(croak);
  3         110  
  3         213  
16 3     3   4243 use Digest::HMAC_MD5;
  3         10748  
  3         155  
17 3     3   4637 use MIME::Base64 qw(decode_base64);
  3         5135  
  3         578  
18              
19             # lib/isccc/include/isccc/cc.h
20 3     3   26 use constant ISCCC_CCMSGTYPE_STRING => 0x00;
  3         7  
  3         217  
21 3     3   17 use constant ISCCC_CCMSGTYPE_BINARYDATA => 0x01;
  3         7  
  3         168  
22 3     3   112 use constant ISCCC_CCMSGTYPE_TABLE => 0x02;
  3         8  
  3         123  
23 3     3   16 use constant ISCCC_CCMSGTYPE_LIST => 0x03;
  3         4  
  3         17074  
24              
25             # Serial should be created by users
26             my $serial = int(rand(2**32));
27              
28             sub new {
29 17     17 1 7775 my ($class, %args) = @_;
30              
31 17         43 my @required_args = qw(
32             key
33             );
34              
35 17         205 my @optional_args = qw(
36             version
37             data
38             nonce
39             );
40              
41 17         40 for my $r (@required_args) {
42 17 100       75 unless (exists $args{$r}) {
43 1         38 croak("Missing required argument '$r'");
44             }
45             }
46              
47 16 100 100     102 if ($args{data} && (ref($args{data}) || '' ) ne 'HASH') {
      100        
48 1         16 croak("Argument 'data' must be a HASH");
49             }
50              
51 15 100 50     60 if (exists $args{version} && ($args{version} || '') !~ /^\d+\z/) {
      66        
52 1         39 croak("Argument 'version' must be a number");
53             }
54              
55 14 100 50     85 if (exists $args{nonce} && ($args{nonce} || '') !~ /^\d+\z/) {
      100        
56 1         12 croak("Argument 'nonce' must be a number");
57             }
58              
59 13   50     131 my %object = (
60             key => $args{key},
61             data => {
62             _ctrl => {
63             _ser => $serial++,
64             },
65             },
66             version => $args{version} || 1,
67             );
68              
69 13 100       40 if ($args{data}) {
70 4         51 $object{data}{_data} = $args{data};
71             } else {
72 9         55 $object{data}{_data}{type} = undef;
73             }
74              
75 13 100       37 if ($args{nonce}) {
76 3         10 $object{data}{_ctrl}{_nonce} = $args{nonce};
77             }
78              
79 13         86 return bless \%object, $class;
80             }
81              
82             sub parse {
83 6     6 1 16 my ($self, $data) = @_;
84              
85 6         20 $self->_set_error('');
86              
87 6 100       27 unless ($self->_cklen($data, 55)) {
88 1         4 return 0;
89             }
90              
91             # Everything after first 51 bytes is what needs to be signed
92 5         19 my $buff = substr($data, 51);
93              
94 5         20 my $length = unpack('N', $data);
95 5         12 $data = substr($data, 4);
96              
97 5 50       16 unless ($self->_cklen($data, $length)) {
98 0         0 return 0;
99             }
100              
101 5         12 my $version = unpack('N', $data);
102 5         9 $data = substr($data, 4);
103              
104 5 50       17 unless ($version == 1) {
105 0         0 return $self->_set_error("Unknown protocol version '$version'");
106             }
107              
108 5         7 my ($aauth, $check);
109              
110             try {
111 5     5   251 $data = _table_fromwire(\$data);
112              
113 5         16 $aauth = $data->{_auth}{hmd5};
114              
115 5         14 $check = $self->_sign($buff);
116             } catch {
117 0     0   0 my $err = $_;
118              
119 0 0       0 if (UNIVERSAL::isa($err, 'Net::RNDC::Exception')) {
120 0         0 $self->_set_error($err);
121             } else {
122 0         0 die $err;
123             }
124 5         56 };
125              
126 5 50       162 return 0 if $self->error;
127              
128 5 50       21 if ($check ne $aauth) {
129 0         0 return $self->_set_error("Couldn't validate response with provided key\n");
130             }
131              
132             try {
133 5     5   218 $self->{data} = _table_fromwire(\$buff);
134             } catch {
135 0     0   0 my $err = $_;
136              
137 0 0       0 if (UNIVERSAL::isa($err, 'Net::RNDC::Exception')) {
138 0         0 $self->_set_error($err);
139             } else {
140 0         0 die $err;
141             }
142 5         40 };
143              
144 5 50       84 return 0 if $self->error;
145              
146 5         25 $self->_set_error($self->{data}->{_data}{err});
147              
148 5 50       14 return $self->error ? 0 : 1;
149             }
150              
151             # Set an error. Uses Net::RNDC::Exception to get file/line number
152             sub _set_error {
153 19     19   42 my ($self, $error) = @_;
154              
155 19 100       57 if (!$error) {
    50          
156 17         41 $self->{error} = '';
157             } elsif (UNIVERSAL::isa($error, 'Net::RNDC::Exception')) {
158 2         9 $self->{error} = $error->error;
159             } else {
160 0         0 my $e = Net::RNDC::Exception->new($error);
161 0         0 $self->{error} = $e->error;
162             }
163              
164 19         55 return 0;
165             }
166              
167             # Return error string if any
168             sub error {
169 23     23 1 729 my ($self) = @_;
170              
171 23         126 return $self->{error};
172             }
173              
174             # Return packet data in binary form
175             sub data {
176 6     6 1 1179 my ($self) = @_;
177              
178 6         19 $self->_set_error('');
179              
180 6         32 $self->{data}->{_ctrl}->{_tim} = time;
181 6         19 $self->{data}->{_ctrl}->{_exp} = time + 60;
182              
183 6         10 my ($udata, $cksum, $wire);
184              
185             try {
186 6     6   315 $udata = $self->_unsigned_data;
187              
188 5         20 $cksum = $self->_sign($udata);
189              
190 5         161 $wire = _table_towire({
191             _auth => {
192             hmd5 => $cksum,
193             },
194             }, 'no_header');
195             } catch {
196 1     1   12 my $err = $_;
197              
198 1 50       21 if (UNIVERSAL::isa($err, 'Net::RNDC::Exception')) {
199 1         4 $self->_set_error($err);
200             } else {
201 0         0 die $err;
202             }
203 6         74 };
204              
205 6 100       129 return if $self->error;
206              
207 5         22 $wire .= $udata;
208              
209 5         49 return pack('N', length($wire) + 4) . pack('N', $self->{version}) . $wire;
210             }
211              
212             # Return the table of data to be signed
213             sub _unsigned_data {
214 6     6   13 my ($self) = @_;
215              
216 6         21 return _table_towire($self->{data}, 'no_header');
217             }
218              
219             # Sign data with our key, return digest
220             sub _sign {
221 10     10   20 my ($self, $data) = @_;
222              
223 10         109 my $hmac = Digest::HMAC_MD5->new(decode_base64($self->{key}));
224              
225 10         7515 $hmac->add($data);
226              
227 10         91 return $hmac->b64digest;
228             }
229              
230             # Take a string from binary format and return it
231             sub _binary_fromwire {
232 49     49   51 my ($wire) = @_;
233              
234 49         63 my $data = $$wire;
235 49         55 $$wire = '';
236              
237 49         240 return $data;
238             }
239              
240             # Pack a string into its binary representation
241             sub _binary_towire {
242 30     30   43 my ($data) = @_;
243              
244 30 100       64 if (!defined $data) {
245 2         9 $data = 'null';
246             }
247              
248 30         165 return pack('c', ISCCC_CCMSGTYPE_BINARYDATA)
249             . pack('N', length($data))
250             . ($data);
251             }
252              
253             # Take a table from binary format and return a hashref
254             sub _table_fromwire {
255 35     35   43 my ($wire) = @_;
256              
257 35         33 my %table;
258              
259 35         200 while ($$wire) {
260 74         129 _cklen_d($$wire, 1);
261 74         131 my $key_len = unpack('c', $$wire);
262 74         131 $$wire = substr($$wire, 1);
263              
264 74         111 _cklen_d($$wire, $key_len);
265 74         120 my $key = substr($$wire, 0, $key_len);
266 74         105 $$wire = substr($$wire, $key_len);
267              
268 74         122 $table{$key} = _value_fromwire($wire);
269             }
270              
271 35         141 return \%table;
272             }
273              
274             # Pack a hashref into its binary representation
275             sub _table_towire {
276 28     28   46 my ($data, $no_header) = @_;
277              
278 28         34 my $table;
279              
280 28         25554 for my $k (sort keys %$data) {
281 48         114 $table .= pack('c', length($k));
282 48         57 $table .= $k;
283 48         108 $table .= _value_towire($data->{$k});
284             }
285              
286 26 100       58 if ($no_header) {
287 10         37 return $table;
288             } else {
289 16         27 my $msg_type = pack('c', ISCCC_CCMSGTYPE_TABLE);
290 16         77 return $msg_type . pack('N', length($table)) . $table;
291             }
292             }
293              
294             # Take a list from binary representation and return an arrayref
295             sub _list_fromwire {
296 0     0   0 my ($wire) = @_;
297              
298 0         0 my @list;
299 0         0 while ($$wire) {
300 0         0 push @list, _value_fromwire($wire);
301             }
302              
303 0         0 return \@list;
304             }
305              
306             # Pack an arrayref into its binary representation
307             sub _list_towire {
308 0     0   0 my ($data) = @_;
309              
310 0         0 my $msg_type = pack('c', ISCCC_CCMSGTYPE_LIST);
311 0         0 my $list;
312              
313 0         0 for my $d (@$data) {
314 0         0 $list .= _value_towire($d);
315             }
316              
317 0         0 return $msg_type . pack('N', length($list)) . $list;
318             }
319              
320             # Take a value, whatever it may be, and unpack it into perl data types
321             sub _value_fromwire {
322 74     74   85 my ($wire) = @_;
323              
324 74         110 _cklen_d($$wire, 5);
325              
326 74         116 my $msg_type = unpack('c', $$wire);
327 74         123 $$wire = substr($$wire, 1);
328              
329 74         104 my $len = unpack('N', $$wire);
330 74         142 $$wire = substr($$wire, 4);
331              
332 74         123 _cklen_d($$wire, $len);
333 74         190 my $data = substr($$wire, 0, $len);
334 74         104 $$wire = substr($$wire, $len);
335              
336 74 100       150 if ($msg_type == ISCCC_CCMSGTYPE_BINARYDATA) {
    50          
    0          
337 49         94 return _binary_fromwire(\$data);
338             } elsif ($msg_type == ISCCC_CCMSGTYPE_TABLE) {
339 25         53 return _table_fromwire(\$data);
340             } elsif ($msg_type == ISCCC_CCMSGTYPE_LIST) {
341 0         0 return _list_fromwire(\$data);
342             } else {
343 0         0 die Net::RNDC::Exception->new(
344             "Unknown message type '$msg_type' in _value_fromwire"
345             );
346             }
347             }
348              
349             # Take a perl data structure and pack it into binary format
350             sub _value_towire {
351 48     48   57 my ($data) = @_;
352              
353 48   100     348 my $r = ref $data || 'binary';
354              
355 48 100       139 if ($r eq 'HASH') {
    50          
    100          
356 17         179 return _table_towire($data);
357             } elsif ($r eq 'ARRAY') {
358 0         0 return _list_towire($data);
359             } elsif ($r eq 'binary') {
360 30         60 return _binary_towire($data);
361             } else {
362 1         14 die Net::RNDC::Exception->new(
363             "Unknown data type '$r' in _value_towire"
364             );
365             }
366             }
367              
368             # Sets an error and returns 0 if the buff isn't at least $len bytes
369             # unless ($self->_cklen($buff, $len)) {
370             # return 0;
371             # }
372             sub _cklen {
373             # my ($self, $buff, $len) = @_;
374              
375 11 100 50 11   44 unless ((length($_[1]) || 0) >= $_[2]) {
376 1         11 $_[0]->_set_error(Net::RNDC::Exception->new(
377             "Unexpected end of data reading buffer. (Expected $_[2] more bytes at least)"
378             ));
379              
380 1         6 return 0;
381             }
382              
383 10         82 return 1;
384             }
385              
386             # Throws an exception if the buff isn't at least $len bytes
387             #
388             # _cklen_d($buff, $len)
389             sub _cklen_d {
390             # my ($buff, $len) = @_;
391              
392 296 50 50 296   798 unless ((length($_[0]) || 0) >= $_[1]) {
393 0           die Net::RNDC::Exception->new(
394             "Unexpected end of data reading buffer. (Expected $_[1] more bytes at least)"
395             );
396             }
397             }
398              
399             1;
400             __END__