1<?php 2 3/** 4 * DNS Library for handling lookups and updates. 5 * 6 * Copyright (c) 2020, Mike Pultz <mike@mikepultz.com>. All rights reserved. 7 * 8 * See LICENSE for more details. 9 * 10 * @category Networking 11 * @package Net_DNS2 12 * @author Mike Pultz <mike@mikepultz.com> 13 * @copyright 2020 Mike Pultz <mike@mikepultz.com> 14 * @license http://www.opensource.org/licenses/bsd-license.php BSD License 15 * @link https://netdns2.com/ 16 * @since File available since Release 0.6.0 17 * 18 */ 19 20/** 21 * This is the base class for DNS Resource Records 22 * 23 * Each resource record type (defined in RR/*.php) extends this class for 24 * base functionality. 25 * 26 * This class handles parsing and constructing the common parts of the DNS 27 * resource records, while the RR specific functionality is handled in each 28 * child class. 29 * 30 * DNS resource record format - RFC1035 section 4.1.3 31 * 32 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 33 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 34 * | | 35 * / / 36 * / NAME / 37 * | | 38 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 39 * | TYPE | 40 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 41 * | CLASS | 42 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 43 * | TTL | 44 * | | 45 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 46 * | RDLENGTH | 47 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| 48 * / RDATA / 49 * / / 50 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 51 * 52 */ 53abstract class Net_DNS2_RR 54{ 55 /* 56 * The name of the resource record 57 */ 58 public $name; 59 60 /* 61 * The resource record type 62 */ 63 public $type; 64 65 /* 66 * The resouce record class 67 */ 68 public $class; 69 70 /* 71 * The time to live for this resource record 72 */ 73 public $ttl; 74 75 /* 76 * The length of the rdata field 77 */ 78 public $rdlength; 79 80 /* 81 * The resource record specific data as a packed binary string 82 */ 83 public $rdata; 84 85 /** 86 * abstract definition - method to return a RR as a string; not to 87 * be confused with the __toString() magic method. 88 * 89 * @return string 90 * @access protected 91 * 92 */ 93 abstract protected function rrToString(); 94 95 /** 96 * abstract definition - parses a RR from a standard DNS config line 97 * 98 * @param array $rdata a string split line of values for the rdata 99 * 100 * @return boolean 101 * @access protected 102 * 103 */ 104 abstract protected function rrFromString(array $rdata); 105 106 /** 107 * abstract definition - sets a Net_DNS2_RR from a Net_DNS2_Packet object 108 * 109 * @param Net_DNS2_Packet &$packet a Net_DNS2_Packet packet to parse the RR from 110 * 111 * @return boolean 112 * @access protected 113 * 114 */ 115 abstract protected function rrSet(Net_DNS2_Packet &$packet); 116 117 /** 118 * abstract definition - returns a binary packet DNS RR object 119 * 120 * @param Net_DNS2_Packet &$packet a Net_DNS2_Packet packet use for 121 * compressed names 122 * 123 * @return mixed either returns a binary packed string or 124 * null on failure 125 * @access protected 126 * 127 */ 128 abstract protected function rrGet(Net_DNS2_Packet &$packet); 129 130 /** 131 * Constructor - builds a new Net_DNS2_RR object 132 * 133 * @param Net_DNS2_Packet &$packet a Net_DNS2_Packet packet or null to create 134 * an empty object 135 * @param array $rr an array with RR parse values or null to 136 * create an empty object 137 * 138 * @throws Net_DNS2_Exception 139 * @access public 140 * 141 */ 142 public function __construct(Net_DNS2_Packet &$packet = null, array $rr = null) 143 { 144 if ( (!is_null($packet)) && (!is_null($rr)) ) { 145 146 if ($this->set($packet, $rr) == false) { 147 148 throw new Net_DNS2_Exception( 149 'failed to generate resource record', 150 Net_DNS2_Lookups::E_RR_INVALID 151 ); 152 } 153 } else { 154 155 $class = Net_DNS2_Lookups::$rr_types_class_to_id[get_class($this)]; 156 if (isset($class)) { 157 158 $this->type = Net_DNS2_Lookups::$rr_types_by_id[$class]; 159 } 160 161 $this->class = 'IN'; 162 $this->ttl = 86400; 163 } 164 } 165 166 /** 167 * magic __toString() method to return the Net_DNS2_RR object object as a string 168 * 169 * @return string 170 * @access public 171 * 172 */ 173 public function __toString() 174 { 175 return $this->name . '. ' . $this->ttl . ' ' . $this->class . 176 ' ' . $this->type . ' ' . $this->rrToString(); 177 } 178 179 /** 180 * return the same data as __toString(), but as an array, so each value can be 181 * used without having to parse the string. 182 * 183 * @return array 184 * @access public 185 * 186 */ 187 public function asArray() 188 { 189 return [ 190 191 'name' => $this->name, 192 'ttl' => $this->ttl, 193 'class' => $this->class, 194 'type' => $this->type, 195 'rdata' => $this->rrToString() 196 ]; 197 } 198 199 /** 200 * return a formatted string; if a string has spaces in it, then return 201 * it with double quotes around it, otherwise, return it as it was passed in. 202 * 203 * @param string $string the string to format 204 * 205 * @return string 206 * @access protected 207 * 208 */ 209 protected function formatString($string) 210 { 211 return '"' . str_replace('"', '\"', trim($string, '"')) . '"'; 212 } 213 214 /** 215 * builds an array of strings from an array of chunks of text split by spaces 216 * 217 * @param array $chunks an array of chunks of text split by spaces 218 * 219 * @return array 220 * @access protected 221 * 222 */ 223 protected function buildString(array $chunks) 224 { 225 $data = []; 226 $c = 0; 227 $in = false; 228 229 foreach ($chunks as $r) { 230 231 $r = trim($r); 232 if (strlen($r) == 0) { 233 continue; 234 } 235 236 if ( ($r[0] == '"') 237 && ($r[strlen($r) - 1] == '"') 238 && ($r[strlen($r) - 2] != '\\') 239 ) { 240 241 $data[$c] = $r; 242 ++$c; 243 $in = false; 244 245 } else if ($r[0] == '"') { 246 247 $data[$c] = $r; 248 $in = true; 249 250 } else if ( ($r[strlen($r) - 1] == '"') 251 && ($r[strlen($r) - 2] != '\\') 252 ) { 253 254 $data[$c] .= ' ' . $r; 255 ++$c; 256 $in = false; 257 258 } else { 259 260 if ($in == true) { 261 $data[$c] .= ' ' . $r; 262 } else { 263 $data[$c++] = $r; 264 } 265 } 266 } 267 268 foreach ($data as $index => $string) { 269 270 $data[$index] = str_replace('\"', '"', trim($string, '"')); 271 } 272 273 return $data; 274 } 275 276 /** 277 * builds a new Net_DNS2_RR object 278 * 279 * @param Net_DNS2_Packet &$packet a Net_DNS2_Packet packet or null to create 280 * an empty object 281 * @param array $rr an array with RR parse values or null to 282 * create an empty object 283 * 284 * @return boolean 285 * @throws Net_DNS2_Exception 286 * @access public 287 * 288 */ 289 public function set(Net_DNS2_Packet &$packet, array $rr) 290 { 291 $this->name = $rr['name']; 292 $this->type = Net_DNS2_Lookups::$rr_types_by_id[$rr['type']]; 293 294 // 295 // for RR OPT (41), the class value includes the requestors UDP payload size, 296 // and not a class value 297 // 298 if ($this->type == 'OPT') { 299 $this->class = $rr['class']; 300 } else { 301 $this->class = Net_DNS2_Lookups::$classes_by_id[$rr['class']]; 302 } 303 304 $this->ttl = $rr['ttl']; 305 $this->rdlength = $rr['rdlength']; 306 $this->rdata = substr($packet->rdata, $packet->offset, $rr['rdlength']); 307 308 return $this->rrSet($packet); 309 } 310 311 /** 312 * returns a binary packed DNS RR object 313 * 314 * @param Net_DNS2_Packet &$packet a Net_DNS2_Packet packet used for 315 * compressing names 316 * 317 * @return string 318 * @throws Net_DNS2_Exception 319 * @access public 320 * 321 */ 322 public function get(Net_DNS2_Packet &$packet) 323 { 324 $data = ''; 325 $rdata = ''; 326 327 // 328 // pack the name 329 // 330 $data = $packet->compress($this->name, $packet->offset); 331 332 // 333 // pack the main values 334 // 335 if ($this->type == 'OPT') { 336 337 // 338 // pre-build the TTL value 339 // 340 $this->preBuild(); 341 342 // 343 // the class value is different for OPT types 344 // 345 $data .= pack( 346 'nnN', 347 Net_DNS2_Lookups::$rr_types_by_name[$this->type], 348 $this->class, 349 $this->ttl 350 ); 351 } else { 352 353 $data .= pack( 354 'nnN', 355 Net_DNS2_Lookups::$rr_types_by_name[$this->type], 356 Net_DNS2_Lookups::$classes_by_name[$this->class], 357 $this->ttl 358 ); 359 } 360 361 // 362 // increase the offset, and allow for the rdlength 363 // 364 $packet->offset += 10; 365 366 // 367 // get the RR specific details 368 // 369 if ($this->rdlength != -1) { 370 371 $rdata = $this->rrGet($packet); 372 } 373 374 // 375 // add the RR 376 // 377 $data .= pack('n', strlen($rdata)) . $rdata; 378 379 return $data; 380 } 381 382 /** 383 * parses a binary packet, and returns the appropriate Net_DNS2_RR object, 384 * based on the RR type of the binary content. 385 * 386 * @param Net_DNS2_Packet &$packet a Net_DNS2_Packet packet used for 387 * decompressing names 388 * 389 * @return mixed returns a new Net_DNS2_RR_* object for 390 * the given RR 391 * @throws Net_DNS2_Exception 392 * @access public 393 * 394 */ 395 public static function parse(Net_DNS2_Packet &$packet) 396 { 397 $object = []; 398 399 // 400 // expand the name 401 // 402 $object['name'] = $packet->expand($packet, $packet->offset); 403 if (is_null($object['name'])) { 404 405 throw new Net_DNS2_Exception( 406 'failed to parse resource record: failed to expand name.', 407 Net_DNS2_Lookups::E_PARSE_ERROR 408 ); 409 } 410 if ($packet->rdlength < ($packet->offset + 10)) { 411 412 throw new Net_DNS2_Exception( 413 'failed to parse resource record: packet too small.', 414 Net_DNS2_Lookups::E_PARSE_ERROR 415 ); 416 } 417 418 // 419 // unpack the RR details 420 // 421 $object['type'] = ord($packet->rdata[$packet->offset++]) << 8 | 422 ord($packet->rdata[$packet->offset++]); 423 $object['class'] = ord($packet->rdata[$packet->offset++]) << 8 | 424 ord($packet->rdata[$packet->offset++]); 425 426 $object['ttl'] = ord($packet->rdata[$packet->offset++]) << 24 | 427 ord($packet->rdata[$packet->offset++]) << 16 | 428 ord($packet->rdata[$packet->offset++]) << 8 | 429 ord($packet->rdata[$packet->offset++]); 430 431 $object['rdlength'] = ord($packet->rdata[$packet->offset++]) << 8 | 432 ord($packet->rdata[$packet->offset++]); 433 434 if ($packet->rdlength < ($packet->offset + $object['rdlength'])) { 435 return null; 436 } 437 438 // 439 // lookup the class to use 440 // 441 $o = null; 442 $class = Net_DNS2_Lookups::$rr_types_id_to_class[$object['type']]; 443 444 if (isset($class)) { 445 446 $o = new $class($packet, $object); 447 if ($o) { 448 449 $packet->offset += $object['rdlength']; 450 } 451 } else { 452 453 throw new Net_DNS2_Exception( 454 'un-implemented resource record type: ' . $object['type'], 455 Net_DNS2_Lookups::E_RR_INVALID 456 ); 457 } 458 459 return $o; 460 } 461 462 /** 463 * cleans up some RR data 464 * 465 * @param string $data the text string to clean 466 * 467 * @return string returns the cleaned string 468 * 469 * @access public 470 * 471 */ 472 public function cleanString($data) 473 { 474 return strtolower(rtrim($data, '.')); 475 } 476 477 /** 478 * parses a standard RR format lines, as defined by rfc1035 (kinda) 479 * 480 * In our implementation, the domain *must* be specified- format must be 481 * 482 * <name> [<ttl>] [<class>] <type> <rdata> 483 * or 484 * <name> [<class>] [<ttl>] <type> <rdata> 485 * 486 * name, title, class and type are parsed by this function, rdata is passed 487 * to the RR specific classes for parsing. 488 * 489 * @param string $line a standard DNS config line 490 * 491 * @return mixed returns a new Net_DNS2_RR_* object for the given RR 492 * @throws Net_DNS2_Exception 493 * @access public 494 * 495 */ 496 public static function fromString($line) 497 { 498 if (strlen($line) == 0) { 499 throw new Net_DNS2_Exception( 500 'empty config line provided.', 501 Net_DNS2_Lookups::E_PARSE_ERROR 502 ); 503 } 504 505 $name = ''; 506 $type = ''; 507 $class = 'IN'; 508 $ttl = 86400; 509 510 // 511 // split the line by spaces 512 // 513 $values = preg_split('/[\s]+/', $line); 514 if (count($values) < 3) { 515 516 throw new Net_DNS2_Exception( 517 'failed to parse config: minimum of name, type and rdata required.', 518 Net_DNS2_Lookups::E_PARSE_ERROR 519 ); 520 } 521 522 // 523 // assume the first value is the name 524 // 525 $name = trim(strtolower(array_shift($values)), '.'); 526 527 // 528 // The next value is either a TTL, Class or Type 529 // 530 foreach ($values as $value) { 531 532 switch(true) { 533 case is_numeric($value): 534 535 $ttl = array_shift($values); 536 break; 537 538 // 539 // this is here because of a bug in is_numeric() in certain versions of 540 // PHP on windows. 541 // 542 case ($value === 0): 543 544 $ttl = array_shift($values); 545 break; 546 547 case isset(Net_DNS2_Lookups::$classes_by_name[strtoupper($value)]): 548 549 $class = strtoupper(array_shift($values)); 550 break; 551 552 case isset(Net_DNS2_Lookups::$rr_types_by_name[strtoupper($value)]): 553 554 $type = strtoupper(array_shift($values)); 555 break 2; 556 break; 557 558 default: 559 560 throw new Net_DNS2_Exception( 561 'invalid config line provided: unknown file: ' . $value, 562 Net_DNS2_Lookups::E_PARSE_ERROR 563 ); 564 } 565 } 566 567 // 568 // lookup the class to use 569 // 570 $o = null; 571 $class_name = Net_DNS2_Lookups::$rr_types_id_to_class[ 572 Net_DNS2_Lookups::$rr_types_by_name[$type] 573 ]; 574 575 if (isset($class_name)) { 576 577 $o = new $class_name; 578 if (!is_null($o)) { 579 580 // 581 // set the parsed values 582 // 583 $o->name = $name; 584 $o->class = $class; 585 $o->ttl = $ttl; 586 587 // 588 // parse the rdata 589 // 590 if ($o->rrFromString($values) === false) { 591 592 throw new Net_DNS2_Exception( 593 'failed to parse rdata for config: ' . $line, 594 Net_DNS2_Lookups::E_PARSE_ERROR 595 ); 596 } 597 598 } else { 599 600 throw new Net_DNS2_Exception( 601 'failed to create new RR record for type: ' . $type, 602 Net_DNS2_Lookups::E_RR_INVALID 603 ); 604 } 605 606 } else { 607 608 throw new Net_DNS2_Exception( 609 'un-implemented resource record type: '. $type, 610 Net_DNS2_Lookups::E_RR_INVALID 611 ); 612 } 613 614 return $o; 615 } 616} 617