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