1package Net::RNDC::Packet;
2{
3  $Net::RNDC::Packet::VERSION = '0.003';
4}
5
6use strict;
7use warnings;
8
9use Net::RNDC::Exception;
10
11use Try::Tiny;
12
13use UNIVERSAL ();
14
15use Carp qw(croak);
16use Digest::HMAC_MD5;
17use MIME::Base64 qw(decode_base64);
18
19# lib/isccc/include/isccc/cc.h
20use constant ISCCC_CCMSGTYPE_STRING     => 0x00;
21use constant ISCCC_CCMSGTYPE_BINARYDATA => 0x01;
22use constant ISCCC_CCMSGTYPE_TABLE      => 0x02;
23use constant ISCCC_CCMSGTYPE_LIST       => 0x03;
24
25# Serial should be created by users
26my $serial = int(rand(2**32));
27
28sub new {
29	my ($class, %args) = @_;
30
31	my @required_args = qw(
32		key
33	);
34
35	my @optional_args = qw(
36		version
37		data
38		nonce
39	);
40
41	for my $r (@required_args) {
42		unless (exists $args{$r}) {
43			croak("Missing required argument '$r'");
44		}
45	}
46
47	if ($args{data} && (ref($args{data}) || '' ) ne 'HASH') {
48		croak("Argument 'data' must be a HASH");
49	}
50
51	if (exists $args{version} && ($args{version} || '') !~ /^\d+\z/) {
52		croak("Argument 'version' must be a number");
53	}
54
55	if (exists $args{nonce} && ($args{nonce} || '') !~ /^\d+\z/) {
56		croak("Argument 'nonce' must be a number");
57	}
58
59	my %object = (
60		key => $args{key},
61		data => {
62			_ctrl => {
63				_ser => $serial++,
64			},
65		},
66		version => $args{version} || 1,
67	);
68
69	if ($args{data}) {
70		$object{data}{_data} = $args{data};
71	} else {
72		$object{data}{_data}{type} = undef;
73	}
74
75	if ($args{nonce}) {
76		$object{data}{_ctrl}{_nonce} = $args{nonce};
77	}
78
79	return bless \%object, $class;
80}
81
82sub parse {
83	my ($self, $data) = @_;
84
85	$self->_set_error('');
86
87	unless ($self->_cklen($data, 55)) {
88		return 0;
89	}
90
91	# Everything after first 51 bytes is what needs to be signed
92	my $buff = substr($data, 51);
93
94	my $length = unpack('N', $data);
95	$data = substr($data, 4);
96
97	unless ($self->_cklen($data, $length)) {
98		return 0;
99	}
100
101	my $version = unpack('N', $data);
102	$data = substr($data, 4);
103
104	unless ($version == 1) {
105		return $self->_set_error("Unknown protocol version '$version'");
106	}
107
108	my ($aauth, $check);
109
110	try {
111		$data = _table_fromwire(\$data);
112
113		$aauth = $data->{_auth}{hmd5};
114
115		$check = $self->_sign($buff);
116	} catch {
117		my $err = $_;
118
119		if (UNIVERSAL::isa($err, 'Net::RNDC::Exception')) {
120			$self->_set_error($err);
121		} else {
122			die $err;
123		}
124	};
125
126	return 0 if $self->error;
127
128	if ($check ne $aauth) {
129		return $self->_set_error("Couldn't validate response with provided key\n");
130	}
131
132	try {
133		$self->{data} = _table_fromwire(\$buff);
134	} catch {
135		my $err = $_;
136
137		if (UNIVERSAL::isa($err, 'Net::RNDC::Exception')) {
138			$self->_set_error($err);
139		} else {
140			die $err;
141		}
142	};
143
144	return 0 if $self->error;
145
146	$self->_set_error($self->{data}->{_data}{err});
147
148	return $self->error ? 0 : 1;
149}
150
151# Set an error. Uses Net::RNDC::Exception to get file/line number
152sub _set_error {
153	my ($self, $error) = @_;
154
155	if (!$error) {
156		$self->{error} = '';
157	} elsif (UNIVERSAL::isa($error, 'Net::RNDC::Exception')) {
158		$self->{error} = $error->error;
159	} else {
160		my $e = Net::RNDC::Exception->new($error);
161		$self->{error} = $e->error;
162	}
163
164	return 0;
165}
166
167# Return error string if any
168sub error {
169	my ($self) = @_;
170
171	return $self->{error};
172}
173
174# Return packet data in binary form
175sub data {
176	my ($self) = @_;
177
178	$self->_set_error('');
179
180	$self->{data}->{_ctrl}->{_tim} = time;
181	$self->{data}->{_ctrl}->{_exp} = time + 60;
182
183	my ($udata, $cksum, $wire);
184
185	try {
186		$udata = $self->_unsigned_data;
187
188		$cksum = $self->_sign($udata);
189
190		$wire = _table_towire({
191			_auth => {
192				hmd5 => $cksum,
193			},
194		}, 'no_header');
195	} catch {
196		my $err = $_;
197
198		if (UNIVERSAL::isa($err, 'Net::RNDC::Exception')) {
199			$self->_set_error($err);
200		} else {
201			die $err;
202		}
203	};
204
205	return if $self->error;
206
207	$wire .= $udata;
208
209	return pack('N', length($wire) + 4) . pack('N', $self->{version}) . $wire;
210}
211
212# Return the table of data to be signed
213sub _unsigned_data {
214	my ($self) = @_;
215
216	return _table_towire($self->{data}, 'no_header');
217}
218
219# Sign data with our key, return digest
220sub _sign {
221	my ($self, $data) = @_;
222
223	my $hmac = Digest::HMAC_MD5->new(decode_base64($self->{key}));
224
225	$hmac->add($data);
226
227	return $hmac->b64digest;
228}
229
230# Take a string from binary format and return it
231sub _binary_fromwire {
232	my ($wire) = @_;
233
234	my $data = $$wire;
235	$$wire = '';
236
237	return $data;
238}
239
240# Pack a string into its binary representation
241sub _binary_towire {
242	my ($data) = @_;
243
244	if (!defined $data) {
245		$data = 'null';
246	}
247
248	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
254sub _table_fromwire {
255	my ($wire) = @_;
256
257	my %table;
258
259	while ($$wire) {
260		_cklen_d($$wire, 1);
261		my $key_len = unpack('c', $$wire);
262		$$wire = substr($$wire, 1);
263
264		_cklen_d($$wire, $key_len);
265		my $key = substr($$wire, 0, $key_len);
266		$$wire = substr($$wire, $key_len);
267
268		$table{$key} = _value_fromwire($wire);
269	}
270
271	return \%table;
272}
273
274# Pack a hashref into its binary representation
275sub _table_towire {
276	my ($data, $no_header) = @_;
277
278	my $table;
279
280	for my $k (sort keys %$data) {
281		$table .= pack('c', length($k));
282		$table .= $k;
283		$table .= _value_towire($data->{$k});
284	}
285
286	if ($no_header) {
287		return $table;
288	} else {
289		my $msg_type = pack('c', ISCCC_CCMSGTYPE_TABLE);
290		return $msg_type . pack('N', length($table)) . $table;
291	}
292}
293
294# Take a list from binary representation and return an arrayref
295sub _list_fromwire {
296	my ($wire) = @_;
297
298	my @list;
299	while ($$wire) {
300		push @list, _value_fromwire($wire);
301	}
302
303	return \@list;
304}
305
306# Pack an arrayref into its binary representation
307sub _list_towire {
308	my ($data) = @_;
309
310	my $msg_type = pack('c', ISCCC_CCMSGTYPE_LIST);
311	my $list;
312
313	for my $d (@$data) {
314		$list .= _value_towire($d);
315	}
316
317	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
321sub _value_fromwire {
322	my ($wire) = @_;
323
324	_cklen_d($$wire, 5);
325
326	my $msg_type = unpack('c', $$wire);
327	$$wire = substr($$wire, 1);
328
329	my $len = unpack('N', $$wire);
330	$$wire = substr($$wire, 4);
331
332	_cklen_d($$wire, $len);
333	my $data = substr($$wire, 0, $len);
334	$$wire = substr($$wire, $len);
335
336	if ($msg_type == ISCCC_CCMSGTYPE_BINARYDATA) {
337		return _binary_fromwire(\$data);
338	} elsif ($msg_type == ISCCC_CCMSGTYPE_TABLE) {
339		return _table_fromwire(\$data);
340	} elsif ($msg_type == ISCCC_CCMSGTYPE_LIST) {
341		return _list_fromwire(\$data);
342	} else {
343		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
350sub _value_towire {
351	my ($data) = @_;
352
353	my $r = ref $data || 'binary';
354
355	if ($r eq 'HASH') {
356		return _table_towire($data);
357	} elsif ($r eq 'ARRAY') {
358		return _list_towire($data);
359	} elsif ($r eq 'binary') {
360		return _binary_towire($data);
361	} else {
362		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# }
372sub _cklen {
373#	my ($self, $buff, $len) = @_;
374
375	unless ((length($_[1]) || 0) >= $_[2]) {
376		$_[0]->_set_error(Net::RNDC::Exception->new(
377			"Unexpected end of data reading buffer. (Expected $_[2] more bytes at least)"
378		));
379
380		return 0;
381	}
382
383	return 1;
384}
385
386# Throws an exception if the buff isn't at least $len bytes
387#
388# _cklen_d($buff, $len)
389sub _cklen_d {
390#	my ($buff, $len) = @_;
391
392	unless ((length($_[0]) || 0) >= $_[1]) {
393		die Net::RNDC::Exception->new(
394			"Unexpected end of data reading buffer. (Expected $_[1] more bytes at least)"
395		);
396	}
397}
398
3991;
400__END__
401
402=head1 NAME
403
404Net::RNDC::Packet - RNDC Protocol V1 Packet Parsing and Generation
405
406=head1 VERSION
407
408version 0.003
409
410=head1 SYNOPSIS
411
412To send an RNDC command and get a response:
413
414  use IO::Socket::INET;
415  use Net::RNDC::Packet;
416
417  my $buff;
418  my $key = 'aabc';
419
420  my $c = IO::Socket::INET->new(
421    PeerAddr => '127.0.0.1:953',
422  ) or die "Failed to create a socket: $@ ($!)";
423
424  # Send opener packet
425  my $pkt = Net::RNDC::Packet->new(
426    key => $key,
427  );
428
429  $c->send($pkt->data);
430
431  # Read nonce response
432  $c->recv($buff, 4096);
433
434  $pkt->parse($buff);
435
436  # Send command request with nonce
437  my $nonce = $pkt->{data}->{_ctrl}{_nonce};
438
439  my $cmd = Net::RNDC::Packet->new(
440    key => $key,
441    nonce => $nonce,
442    data => {type => 'status'},
443  );
444
445  $c->send($cmd->data);
446
447  # Read final response
448  $c->recv($buff, 4096);
449
450  $cmd->parse($buff);
451
452  my $resp = $cmd->{data}{_data}{text} || 'command success';
453
454  print "$resp\n";
455
456=head1 DESCRIPTION
457
458This package provides low-level RNDC V1 protocol parsing and generation. It
459allows full control over the data in the sent/received packets.
460
461Currently this is provided by hacking at C<< $pkt->{data} >>, setter/getter
462methods will be forthcoming.
463
464=head2 Constructor
465
466=head3 new
467
468  my $packet = Net::RNDC::Packet->new(%args);
469
470Arguments:
471
472=over 4
473
474=item *
475
476B<key> - The Base64 encoded HMAC-MD5 key to sign/verify packets with.
477
478=item *
479
480B<data> - A hashref of data to put in the request of the packet. Currently, BIND
481only understand commands in the C<type> key. For example:
482
483  data => { type => 'status' },
484
485=item *
486
487B<nonce> - The nonce data returned from the remote nameserver. Located in the
488parsed packet in the _ctrl section:
489
490  nonce => $packet->{data}->{_ctrl}{_nonce},
491
492=back
493
494=head2 Methods
495
496=head3 data
497
498  my $binary = $packet->data;
499
500Generates a binary representation of the packet, suitable for sending over the
501wire.
502
503=head3 parse
504
505  if ($packet->parse($binary)) { ... }
506
507Parses data from the wire and populates the current packet with the information,
508as well as verifies the data with the provided B<key> that was passed to the
509constructor. Returns 1 on success, 0 on failure. Check L</error> if there's a
510failure.
511
512=head3 error
513
514  my $err = $packet->error;
515
516Returns a string error, if any, after packet parsing or generation failed.
517
518=head1 TODO
519
520=over 4
521
522=item *
523
524Methods for modifying the different data parts of an RNDC message
525
526=back
527
528=head1 SEE ALSO
529
530L<Net::RNDC> - Simple RNDC communication.
531
532L<Net::RNDC::Session> - Manage the 4-packet RNDC session
533
534=head1 AUTHOR
535
536Matthew Horsfall (alh) <WolfSage@gmail.com>
537
538=head1 LICENSE
539
540You may distribute this code under the same terms as Perl itself.
541
542=cut
543