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