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