1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
4/**
5 * DNS Library for handling lookups and updates.
6 *
7 * PHP Version 5
8 *
9 * Copyright (c) 2010, Mike Pultz <mike@mikepultz.com>.
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions
14 * are met:
15 *
16 *   * Redistributions of source code must retain the above copyright
17 *     notice, this list of conditions and the following disclaimer.
18 *
19 *   * Redistributions in binary form must reproduce the above copyright
20 *     notice, this list of conditions and the following disclaimer in
21 *     the documentation and/or other materials provided with the
22 *     distribution.
23 *
24 *   * Neither the name of Mike Pultz nor the names of his contributors
25 *     may be used to endorse or promote products derived from this
26 *     software without specific prior written permission.
27 *
28 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
31 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
32 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
33 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
34 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
35 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
36 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRIC
37 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
38 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
39 * POSSIBILITY OF SUCH DAMAGE.
40 *
41 * @category  Networking
42 * @package   Net_DNS2
43 * @author    Mike Pultz <mike@mikepultz.com>
44 * @copyright 2010 Mike Pultz <mike@mikepultz.com>
45 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License
46 * @version   SVN: $Id$
47 * @link      http://pear.php.net/package/Net_DNS2
48 * @since     File available since Release 0.6.0
49 *
50 */
51
52/*
53 * register the auto-load function
54 *
55 */
56spl_autoload_register('Net_DNS2::autoload');
57
58/**
59 * This is the base class for the Net_DNS2_Resolver and Net_DNS2_Updater
60 * classes.
61 *
62 * @category Networking
63 * @package  Net_DNS2
64 * @author   Mike Pultz <mike@mikepultz.com>
65 * @license  http://www.opensource.org/licenses/bsd-license.php  BSD License
66 * @link     http://pear.php.net/package/Net_DNS2
67 * @see      Net_DNS2_Resolver, Net_DNS2_Updater
68 *
69 */
70class Net_DNS2
71{
72    /*
73     * the current version of this library
74     */
75    const VERSION = '1.4.3';
76
77    /*
78     * the default path to a resolv.conf file
79     */
80    const RESOLV_CONF = '/etc/resolv.conf';
81
82    /*
83     * override options from the resolv.conf file
84     *
85     * if this is set, then certain values from the resolv.conf file will override
86     * local settings. This is disabled by default to remain backwards compatible.
87     *
88     */
89    public $use_resolv_options = false;
90
91    /*
92     * use TCP only (true/false)
93     */
94    public $use_tcp = false;
95
96    /*
97     * DNS Port to use (53)
98     */
99    public $dns_port = 53;
100
101    /*
102     * the ip/port for use as a local socket
103     */
104    public $local_host = '';
105    public $local_port = 0;
106
107    /*
108     * timeout value for socket connections
109     */
110    public $timeout = 5;
111
112    /*
113     * randomize the name servers list
114     */
115    public $ns_random = false;
116
117    /*
118     * default domains
119     */
120    public $domain = '';
121
122    /*
123     * domain search list - not actually used right now
124     */
125    public $search_list = array();
126
127    /*
128     * enable cache; either "shared", "file" or "none"
129     */
130    public $cache_type = 'none';
131
132    /*
133     * file name to use for shared memory segment or file cache
134     */
135    public $cache_file = '/tmp/net_dns2.cache';
136
137    /*
138     * the max size of the cache file (in bytes)
139     */
140    public $cache_size = 50000;
141
142    /*
143     * the method to use for storing cache data; either "serialize" or "json"
144     *
145     * json is faster, but can't remember the class names (everything comes back
146     * as a "stdClass Object"; all the data is the same though. serialize is
147     * slower, but will have all the class info.
148     *
149     * defaults to 'serialize'
150     */
151    public $cache_serializer = 'serialize';
152
153    /*
154     * by default, according to RFC 1034
155     *
156     * CNAME RRs cause special action in DNS software.  When a name server
157     * fails to find a desired RR in the resource set associated with the
158     * domain name, it checks to see if the resource set consists of a CNAME
159     * record with a matching class.  If so, the name server includes the CNAME
160     * record in the response and restarts the query at the domain name
161     * specified in the data field of the CNAME record.
162     *
163     * this can cause "unexpected" behavious, since i'm sure *most* people
164     * don't know DNS does this; there may be cases where Net_DNS2 returns a
165     * positive response, even though the hostname the user looked up did not
166     * actually exist.
167     *
168     * strict_query_mode means that if the hostname that was looked up isn't
169     * actually in the answer section of the response, Net_DNS2 will return an
170     * empty answer section, instead of an answer section that could contain
171     * CNAME records.
172     *
173     */
174    public $strict_query_mode = false;
175
176    /*
177     * if we should set the recursion desired bit to 1 or 0.
178     *
179     * by default this is set to true, we want the DNS server to perform a recursive
180     * request. If set to false, the RD bit will be set to 0, and the server will
181     * not perform recursion on the request.
182     */
183    public $recurse = true;
184
185    /*
186     * request DNSSEC values, by setting the DO flag to 1; this actually makes
187     * the resolver add a OPT RR to the additional section, and sets the DO flag
188     * in this RR to 1
189     *
190     */
191    public $dnssec = false;
192
193    /*
194     * set the DNSSEC AD (Authentic Data) bit on/off; the AD bit on the request
195     * side was previously undefined, and resolvers we instructed to always clear
196     * the AD bit when sending a request.
197     *
198     * RFC6840 section 5.7 defines setting the AD bit in the query as a signal to
199     * the server that it wants the value of the AD bit, without needed to request
200     * all the DNSSEC data via the DO bit.
201     *
202     */
203    public $dnssec_ad_flag = false;
204
205    /*
206     * set the DNSSEC CD (Checking Disabled) bit on/off; turning this off, means
207     * that the DNS resolver will perform it's own signature validation- so the DNS
208     * servers simply pass through all the details.
209     *
210     */
211    public $dnssec_cd_flag = false;
212
213    /*
214     * the EDNS(0) UDP payload size to use when making DNSSEC requests
215     * see RFC 4035 section 4.1 - EDNS Support.
216     *
217     * there is some different ideas on the suggest size to supprt; but it seems to
218     * be "at least 1220 bytes, but SHOULD support 4000 bytes.
219     *
220     * we'll just support 4000
221     *
222     */
223    public $dnssec_payload_size = 4000;
224
225    /*
226     * the last exeception that was generated
227     */
228    public $last_exception = null;
229
230    /*
231     * the list of exceptions by name server
232     */
233    public $last_exception_list = array();
234
235    /*
236     * name server list
237     */
238    public $nameservers = array();
239
240    /*
241     * local sockets
242     */
243    protected $sock = array(Net_DNS2_Socket::SOCK_DGRAM => array(), Net_DNS2_Socket::SOCK_STREAM => array());
244
245    /*
246     * if the socket extension is loaded
247     */
248    protected $sockets_enabled = false;
249
250    /*
251     * the TSIG or SIG RR object for authentication
252     */
253    protected $auth_signature = null;
254
255    /*
256     * the shared memory segment id for the local cache
257     */
258    protected $cache = null;
259
260    /*
261     * internal setting for enabling cache
262     */
263    protected $use_cache = false;
264
265    /**
266     * Constructor - base constructor for the Resolver and Updater
267     *
268     * @param mixed $options array of options or null for none
269     *
270     * @throws Net_DNS2_Exception
271     * @access public
272     *
273     */
274    public function __construct(array $options = null)
275    {
276        //
277        // check for the sockets extension; we no longer support the sockets library under
278        // windows- there have been too many errors related to sockets under windows-
279        // specifically inconsistent socket defines between versions of windows-
280        //
281        // and since I can't seem to find a way to get the actual windows version, it
282        // doesn't seem fixable in the code.
283        //
284        if ( (extension_loaded('sockets') == true) && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') ) {
285
286            $this->sockets_enabled = true;
287        }
288
289        //
290        // load any options that were provided
291        //
292        if (!empty($options)) {
293
294            foreach ($options as $key => $value) {
295
296                if ($key == 'nameservers') {
297
298                    $this->setServers($value);
299                } else {
300
301                    $this->$key = $value;
302                }
303            }
304        }
305
306        //
307        // if we're set to use the local shared memory cache, then
308        // make sure it's been initialized
309        //
310        switch($this->cache_type) {
311        case 'shared':
312            if (extension_loaded('shmop')) {
313
314                $this->cache = new Net_DNS2_Cache_Shm;
315                $this->use_cache = true;
316            } else {
317
318                throw new Net_DNS2_Exception(
319                    'shmop library is not available for cache',
320                    Net_DNS2_Lookups::E_CACHE_SHM_UNAVAIL
321                );
322            }
323            break;
324        case 'file':
325
326            $this->cache = new Net_DNS2_Cache_File;
327            $this->use_cache = true;
328
329            break;
330        case 'none':
331            $this->use_cache = false;
332            break;
333        default:
334
335            throw new Net_DNS2_Exception(
336                'un-supported cache type: ' . $this->cache_type,
337                Net_DNS2_Lookups::E_CACHE_UNSUPPORTED
338            );
339        }
340    }
341
342    /**
343     * autoload call-back function; used to auto-load classes
344     *
345     * @param string $name the name of the class
346     *
347     * @return void
348     * @access public
349     *
350     */
351    static public function autoload($name)
352    {
353        //
354        // only auto-load our classes
355        //
356        if (strncmp($name, 'Net_DNS2', 8) == 0) {
357
358            include dirname(__FILE__) . '/../' . str_replace('_', '/', $name) . '.php';
359        }
360
361        return;
362    }
363
364    /**
365     * sets the name servers to be used
366     *
367     * @param mixed $nameservers either an array of name servers, or a file name
368     *                           to parse, assuming it's in the resolv.conf format
369     *
370     * @return boolean
371     * @throws Net_DNS2_Exception
372     * @access public
373     *
374     */
375    public function setServers($nameservers)
376    {
377        //
378        // if it's an array, then use it directly
379        //
380        // otherwise, see if it's a path to a resolv.conf file and if so, load it
381        //
382        if (is_array($nameservers)) {
383
384            $this->nameservers = $nameservers;
385
386        } else {
387
388            //
389            // temporary list of name servers; do it this way rather than just
390            // resetting the local nameservers value, just incase an exception
391            // is thrown here; this way we might avoid ending up with an empty
392            // namservers list.
393            //
394            $ns = array();
395
396            //
397            // check to see if the file is readable
398            //
399            if (is_readable($nameservers) === true) {
400
401                $data = file_get_contents($nameservers);
402                if ($data === false) {
403                    throw new Net_DNS2_Exception(
404                        'failed to read contents of file: ' . $nameservers,
405                        Net_DNS2_Lookups::E_NS_INVALID_FILE
406                    );
407                }
408
409                $lines = explode("\n", $data);
410
411                foreach ($lines as $line) {
412
413                    $line = trim($line);
414
415                    //
416                    // ignore empty lines, and lines that are commented out
417                    //
418                    if ( (strlen($line) == 0)
419                        || ($line[0] == '#')
420                        || ($line[0] == ';')
421                    ) {
422                        continue;
423                    }
424
425                    //
426                    // ignore lines with no spaces in them.
427                    //
428                    if (strpos($line, ' ') === false) {
429                        continue;
430                    }
431
432                    list($key, $value) = preg_split('/\s+/', $line, 2);
433
434                    $key    = trim(strtolower($key));
435                    $value  = trim(strtolower($value));
436
437                    switch($key) {
438                    case 'nameserver':
439
440                        //
441                        // nameserver can be a IPv4 or IPv6 address
442                        //
443                        if ( (self::isIPv4($value) == true)
444                            || (self::isIPv6($value) == true)
445                        ) {
446
447                            $ns[] = $value;
448                        } else {
449
450                            throw new Net_DNS2_Exception(
451                                'invalid nameserver entry: ' . $value,
452                                Net_DNS2_Lookups::E_NS_INVALID_ENTRY
453                            );
454                        }
455                        break;
456
457                    case 'domain':
458                        $this->domain = $value;
459                        break;
460
461                    case 'search':
462                        $this->search_list = preg_split('/\s+/', $value);
463                        break;
464
465                    case 'options':
466                        $this->parseOptions($value);
467                        break;
468
469                    default:
470                        ;
471                    }
472                }
473
474                //
475                // if we don't have a domain, but we have a search list, then
476                // take the first entry on the search list as the domain
477                //
478                if ( (strlen($this->domain) == 0)
479                    && (count($this->search_list) > 0)
480                ) {
481                    $this->domain = $this->search_list[0];
482                }
483
484            } else {
485                throw new Net_DNS2_Exception(
486                    'resolver file file provided is not readable: ' . $nameservers,
487                    Net_DNS2_Lookups::E_NS_INVALID_FILE
488                );
489            }
490
491            //
492            // store the name servers locally
493            //
494            if (count($ns) > 0) {
495                $this->nameservers = $ns;
496            }
497        }
498
499        //
500        // remove any duplicates; not sure if we should bother with this- if people
501        // put duplicate name servers, who I am to stop them?
502        //
503        $this->nameservers = array_unique($this->nameservers);
504
505        //
506        // check the name servers
507        //
508        $this->checkServers();
509
510        return true;
511    }
512
513    /**
514     * parses the options line from a resolv.conf file; we don't support all the options
515     * yet, and using them is optional.
516     *
517     * @param string $value is the options string from the resolv.conf file.
518     *
519     * @return boolean
520     * @access private
521     *
522     */
523    private function parseOptions($value)
524    {
525        //
526        // if overrides are disabled (the default), or the options list is empty for some
527        // reason, then we don't need to do any of this work.
528        //
529        if ( ($this->use_resolv_options == false) || (strlen($value) == 0) ) {
530
531            return true;
532        }
533
534        $options = preg_split('/\s+/', strtolower($value));
535
536        foreach ($options as $option) {
537
538            //
539            // override the timeout value from the resolv.conf file.
540            //
541            if ( (strncmp($option, 'timeout', 7) == 0) && (strpos($option, ':') !== false) ) {
542
543                list($key, $val) = explode(':', $option);
544
545                if ( ($val > 0) && ($val <= 30) ) {
546
547                    $this->timeout = $val;
548                }
549
550            //
551            // the rotate option just enabled the ns_random option
552            //
553            } else if (strncmp($option, 'rotate', 6) == 0) {
554
555                $this->ns_random = true;
556            }
557        }
558
559        return true;
560    }
561
562    /**
563     * checks the list of name servers to make sure they're set
564     *
565     * @param mixed $default a path to a resolv.conf file or an array of servers.
566     *
567     * @return boolean
568     * @throws Net_DNS2_Exception
569     * @access protected
570     *
571     */
572    protected function checkServers($default = null)
573    {
574        if (empty($this->nameservers)) {
575
576            if (isset($default)) {
577
578                $this->setServers($default);
579            } else {
580
581                throw new Net_DNS2_Exception(
582                    'empty name servers list; you must provide a list of name '.
583                    'servers, or the path to a resolv.conf file.',
584                    Net_DNS2_Lookups::E_NS_INVALID_ENTRY
585                );
586            }
587        }
588
589        return true;
590    }
591
592    /**
593     * adds a TSIG RR object for authentication
594     *
595     * @param string $keyname   the key name to use for the TSIG RR
596     * @param string $signature the key to sign the request.
597     * @param string $algorithm the algorithm to use
598     *
599     * @return boolean
600     * @access public
601     * @since  function available since release 1.1.0
602     *
603     */
604    public function signTSIG(
605        $keyname, $signature = '', $algorithm = Net_DNS2_RR_TSIG::HMAC_MD5
606    ) {
607        //
608        // if the TSIG was pre-created and passed in, then we can just used
609        // it as provided.
610        //
611        if ($keyname instanceof Net_DNS2_RR_TSIG) {
612
613            $this->auth_signature = $keyname;
614
615        } else {
616
617            //
618            // otherwise create the TSIG RR, but don't add it just yet; TSIG needs
619            // to be added as the last additional entry- so we'll add it just
620            // before we send.
621            //
622            $this->auth_signature = Net_DNS2_RR::fromString(
623                strtolower(trim($keyname)) .
624                ' TSIG '. $signature
625            );
626
627            //
628            // set the algorithm to use
629            //
630            $this->auth_signature->algorithm = $algorithm;
631        }
632
633        return true;
634    }
635
636    /**
637     * adds a SIG RR object for authentication
638     *
639     * @param string $filename the name of a file to load the signature from.
640     *
641     * @return boolean
642     * @throws Net_DNS2_Exception
643     * @access public
644     * @since  function available since release 1.1.0
645     *
646     */
647    public function signSIG0($filename)
648    {
649        //
650        // check for OpenSSL
651        //
652        if (extension_loaded('openssl') === false) {
653
654            throw new Net_DNS2_Exception(
655                'the OpenSSL extension is required to use SIG(0).',
656                Net_DNS2_Lookups::E_OPENSSL_UNAVAIL
657            );
658        }
659
660        //
661        // if the SIG was pre-created, then use it as-is
662        //
663        if ($filename instanceof Net_DNS2_RR_SIG) {
664
665            $this->auth_signature = $filename;
666
667        } else {
668
669            //
670            // otherwise, it's filename which needs to be parsed and processed.
671            //
672            $private = new Net_DNS2_PrivateKey($filename);
673
674            //
675            // create a new Net_DNS2_RR_SIG object
676            //
677            $this->auth_signature = new Net_DNS2_RR_SIG();
678
679            //
680            // reset some values
681            //
682            $this->auth_signature->name         = $private->signname;
683            $this->auth_signature->ttl          = 0;
684            $this->auth_signature->class        = 'ANY';
685
686            //
687            // these values are pulled from the private key
688            //
689            $this->auth_signature->algorithm    = $private->algorithm;
690            $this->auth_signature->keytag       = $private->keytag;
691            $this->auth_signature->signname     = $private->signname;
692
693            //
694            // these values are hard-coded for SIG0
695            //
696            $this->auth_signature->typecovered  = 'SIG0';
697            $this->auth_signature->labels       = 0;
698            $this->auth_signature->origttl      = 0;
699
700            //
701            // generate the dates
702            //
703            $t = time();
704
705            $this->auth_signature->sigincep     = gmdate('YmdHis', $t);
706            $this->auth_signature->sigexp       = gmdate('YmdHis', $t + 500);
707
708            //
709            // store the private key in the SIG object for later.
710            //
711            $this->auth_signature->private_key  = $private;
712        }
713
714        //
715        // only RSA algorithms are supported for SIG(0)
716        //
717        switch($this->auth_signature->algorithm) {
718        case Net_DNS2_Lookups::DNSSEC_ALGORITHM_RSAMD5:
719        case Net_DNS2_Lookups::DNSSEC_ALGORITHM_RSASHA1:
720        case Net_DNS2_Lookups::DNSSEC_ALGORITHM_RSASHA256:
721        case Net_DNS2_Lookups::DNSSEC_ALGORITHM_RSASHA512:
722        case Net_DNS2_Lookups::DNSSEC_ALGORITHM_DSA:
723            break;
724        default:
725            throw new Net_DNS2_Exception(
726                'only asymmetric algorithms work with SIG(0)!',
727                Net_DNS2_Lookups::E_OPENSSL_INV_ALGO
728            );
729        }
730
731        return true;
732    }
733
734    /**
735     * a simple function to determine if the RR type is cacheable
736     *
737     * @param stream $_type the RR type string
738     *
739     * @return bool returns true/false if the RR type if cachable
740     * @access public
741     *
742     */
743    public function cacheable($_type)
744    {
745        switch($_type) {
746        case 'AXFR':
747        case 'OPT':
748            return false;
749        }
750
751        return true;
752    }
753
754    /**
755     * PHP doesn't support unsigned integers, but many of the RR's return
756     * unsigned values (like SOA), so there is the possibility that the
757     * value will overrun on 32bit systems, and you'll end up with a
758     * negative value.
759     *
760     * 64bit systems are not affected, as their PHP_IN_MAX value should
761     * be 64bit (ie 9223372036854775807)
762     *
763     * This function returns a negative integer value, as a string, with
764     * the correct unsigned value.
765     *
766     * @param string $_int the unsigned integer value to check
767     *
768     * @return string returns the unsigned value as a string.
769     * @access public
770     *
771     */
772    public static function expandUint32($_int)
773    {
774        if ( ($_int < 0) && (PHP_INT_MAX == 2147483647) ) {
775            return sprintf('%u', $_int);
776        } else {
777            return $_int;
778        }
779    }
780
781    /**
782     * returns true/false if the given address is a valid IPv4 address
783     *
784     * @param string $_address the IPv4 address to check
785     *
786     * @return boolean returns true/false if the address is IPv4 address
787     * @access public
788     *
789     */
790    public static function isIPv4($_address)
791    {
792        //
793        // use filter_var() if it's available; it's faster than preg
794        //
795        if (extension_loaded('filter') == true) {
796
797            if (filter_var($_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) == false) {
798                return false;
799            }
800        } else {
801
802            //
803            // do the main check here;
804            //
805            if (inet_pton($_address) === false) {
806                return false;
807            }
808
809            //
810            // then make sure we're not a IPv6 address
811            //
812            if (preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $_address) == 0) {
813                return false;
814            }
815        }
816
817        return true;
818    }
819
820    /**
821     * returns true/false if the given address is a valid IPv6 address
822     *
823     * @param string $_address the IPv6 address to check
824     *
825     * @return boolean returns true/false if the address is IPv6 address
826     * @access public
827     *
828     */
829    public static function isIPv6($_address)
830    {
831        //
832        // use filter_var() if it's available; it's faster than preg
833        //
834        if (extension_loaded('filter') == true) {
835            if (filter_var($_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) == false) {
836                return false;
837            }
838        } else {
839
840            //
841            // do the main check here
842            //
843            if (inet_pton($_address) === false) {
844                return false;
845            }
846
847            //
848            // then make sure it doesn't match a IPv4 address
849            //
850            if (preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $_address) == 1) {
851                return false;
852            }
853        }
854
855        return true;
856    }
857
858    /**
859     * formats the given IPv6 address as a fully expanded IPv6 address
860     *
861     * @param string $_address the IPv6 address to expand
862     *
863     * @return string the fully expanded IPv6 address
864     * @access public
865     *
866     */
867    public static function expandIPv6($_address)
868    {
869        $hex = unpack('H*hex', inet_pton($_address));
870
871        return substr(preg_replace('/([A-f0-9]{4})/', "$1:", $hex['hex']), 0, -1);
872    }
873
874    /**
875     * sends a standard Net_DNS2_Packet_Request packet
876     *
877     * @param Net_DNS2_Packet $request a Net_DNS2_Packet_Request object
878     * @param boolean         $use_tcp true/false if the function should
879     *                                 use TCP for the request
880     *
881     * @return mixed returns a Net_DNS2_Packet_Response object, or false on error
882     * @throws Net_DNS2_Exception
883     * @access protected
884     *
885     */
886    protected function sendPacket(Net_DNS2_Packet $request, $use_tcp)
887    {
888        //
889        // get the data from the packet
890        //
891        $data = $request->get();
892        if (strlen($data) < Net_DNS2_Lookups::DNS_HEADER_SIZE) {
893
894            throw new Net_DNS2_Exception(
895                'invalid or empty packet for sending!',
896                Net_DNS2_Lookups::E_PACKET_INVALID,
897                null,
898                $request
899            );
900        }
901
902        reset($this->nameservers);
903
904        //
905        // randomize the name server list if it's asked for
906        //
907        if ($this->ns_random == true) {
908
909            shuffle($this->nameservers);
910        }
911
912        //
913        // loop so we can handle server errors
914        //
915        $response = null;
916        $ns = false;
917        // php72 compatibility - Reset pointer to start of array
918        reset($this->nameservers);
919
920        while (1) {
921
922            //
923            // grab the next DNS server
924            //
925            // $ns = each($this->nameservers);
926
927            // php72 compatibility - grab the next DNS server
928            $ns = current($this->nameservers);
929            next($this->nameservers);
930
931            if ($ns === false) {
932
933                if (is_null($this->last_exception) == false) {
934
935                    throw $this->last_exception;
936                } else {
937
938                    throw new Net_DNS2_Exception(
939                        'every name server provided has failed',
940                        Net_DNS2_Lookups::E_NS_FAILED
941                    );
942                }
943            }
944
945            // php72 compatibility - each() returns array(0=>key, 1=>value), current() returns value.
946            // $ns = $ns[1];
947
948            //
949            // if the use TCP flag (force TCP) is set, or the packet is bigger than our
950            // max allowed UDP size- which is either 512, or if this is DNSSEC request,
951            // then whatever the configured dnssec_payload_size is.
952            //
953            $max_udp_size = Net_DNS2_Lookups::DNS_MAX_UDP_SIZE;
954            if ($this->dnssec == true)
955            {
956                $max_udp_size = $this->dnssec_payload_size;
957            }
958
959            if ( ($use_tcp == true) || (strlen($data) > $max_udp_size) ) {
960
961                try
962                {
963                    $response = $this->sendTCPRequest($ns, $data, ($request->question[0]->qtype == 'AXFR') ? true : false);
964
965                } catch(Net_DNS2_Exception $e) {
966
967                    $this->last_exception = $e;
968                    $this->last_exception_list[$ns] = $e;
969
970                    continue;
971                }
972
973            //
974            // otherwise, send it using UDP
975            //
976            } else {
977
978                try
979                {
980                    $response = $this->sendUDPRequest($ns, $data);
981
982                    //
983                    // check the packet header for a trucated bit; if it was truncated,
984                    // then re-send the request as TCP.
985                    //
986                    if ($response->header->tc == 1) {
987
988                        $response = $this->sendTCPRequest($ns, $data);
989                    }
990
991                } catch(Net_DNS2_Exception $e) {
992
993                    $this->last_exception = $e;
994                    $this->last_exception_list[$ns] = $e;
995
996                    continue;
997                }
998            }
999
1000            //
1001            // make sure header id's match between the request and response
1002            //
1003            if ($request->header->id != $response->header->id) {
1004
1005                $this->last_exception = new Net_DNS2_Exception(
1006
1007                    'invalid header: the request and response id do not match.',
1008                    Net_DNS2_Lookups::E_HEADER_INVALID,
1009                    null,
1010                    $request,
1011                    $response
1012                );
1013
1014                $this->last_exception_list[$ns] = $this->last_exception;
1015                continue;
1016            }
1017
1018            //
1019            // make sure the response is actually a response
1020            //
1021            // 0 = query, 1 = response
1022            //
1023            if ($response->header->qr != Net_DNS2_Lookups::QR_RESPONSE) {
1024
1025                $this->last_exception = new Net_DNS2_Exception(
1026
1027                    'invalid header: the response provided is not a response packet.',
1028                    Net_DNS2_Lookups::E_HEADER_INVALID,
1029                    null,
1030                    $request,
1031                    $response
1032                );
1033
1034                $this->last_exception_list[$ns] = $this->last_exception;
1035                continue;
1036            }
1037
1038            //
1039            // make sure the response code in the header is ok
1040            //
1041            if ($response->header->rcode != Net_DNS2_Lookups::RCODE_NOERROR) {
1042
1043                $this->last_exception = new Net_DNS2_Exception(
1044
1045                    'DNS request failed: ' .
1046                    Net_DNS2_Lookups::$result_code_messages[$response->header->rcode],
1047                    $response->header->rcode,
1048                    null,
1049                    $request,
1050                    $response
1051                );
1052
1053                $this->last_exception_list[$ns] = $this->last_exception;
1054                continue;
1055            }
1056
1057            break;
1058        }
1059
1060        return $response;
1061    }
1062
1063    /**
1064     * cleans up a failed socket and throws the given exception
1065     *
1066     * @param string  $_proto the protocol of the socket
1067     * @param string  $_ns    the name server to use for the request
1068     * @param string  $_error the error message to throw at the end of the function
1069     *
1070     * @throws Net_DNS2_Exception
1071     * @access private
1072     *
1073     */
1074    private function generateError($_proto, $_ns, $_error)
1075    {
1076        if (isset($this->sock[$_proto][$_ns]) == false)
1077        {
1078            throw new Net_DNS2_Exception('invalid socket referenced', Net_DNS2_Lookups::E_NS_INVALID_SOCKET);
1079        }
1080
1081        //
1082        // grab the last error message off the socket
1083        //
1084        $last_error = $this->sock[$_proto][$_ns]->last_error;
1085
1086        //
1087        // close it
1088        //
1089        $this->sock[$_proto][$_ns]->close();
1090
1091        //
1092        // remove it from the socket cache
1093        //
1094        unset($this->sock[$_proto][$_ns]);
1095
1096        //
1097        // throw the error provided
1098        //
1099        throw new Net_DNS2_Exception($last_error, $_error);
1100    }
1101
1102    /**
1103     * sends a DNS request using TCP
1104     *
1105     * @param string  $_ns   the name server to use for the request
1106     * @param string  $_data the raw DNS packet data
1107     * @param boolean $_axfr if this is a zone transfer request
1108     *
1109     * @return Net_DNS2_Packet_Response the reponse object
1110     * @throws Net_DNS2_Exception
1111     * @access private
1112     *
1113     */
1114    private function sendTCPRequest($_ns, $_data, $_axfr = false)
1115    {
1116        //
1117        // grab the start time
1118        //
1119        $start_time = microtime(true);
1120
1121        //
1122        // see if we already have an open socket from a previous request; if so, try to use
1123        // that instead of opening a new one.
1124        //
1125        if ( (!isset($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]))
1126            || (!($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns] instanceof Net_DNS2_Socket))
1127        ) {
1128
1129            //
1130            // if the socket library is available, then use that
1131            //
1132            if ($this->sockets_enabled === true) {
1133
1134                $this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns] = new Net_DNS2_Socket_Sockets(
1135                    Net_DNS2_Socket::SOCK_STREAM, $_ns, $this->dns_port, $this->timeout
1136                );
1137
1138            //
1139            // otherwise the streams library
1140            //
1141            } else {
1142
1143                $this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns] = new Net_DNS2_Socket_Streams(
1144                    Net_DNS2_Socket::SOCK_STREAM, $_ns, $this->dns_port, $this->timeout
1145                );
1146            }
1147
1148            //
1149            // if a local IP address / port is set, then add it
1150            //
1151            if (strlen($this->local_host) > 0) {
1152
1153                $this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->bindAddress(
1154                    $this->local_host, $this->local_port
1155                );
1156            }
1157
1158            //
1159            // open the socket
1160            //
1161            if ($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->open() === false) {
1162
1163                $this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1164            }
1165        }
1166
1167        //
1168        // write the data to the socket; if it fails, continue on
1169        // the while loop
1170        //
1171        if ($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->write($_data) === false) {
1172
1173            $this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1174        }
1175
1176        //
1177        // read the content, using select to wait for a response
1178        //
1179        $size = 0;
1180        $result = null;
1181        $response = null;
1182
1183        //
1184        // handle zone transfer requests differently than other requests.
1185        //
1186        if ($_axfr == true) {
1187
1188            $soa_count = 0;
1189
1190            while (1) {
1191
1192                //
1193                // read the data off the socket
1194                //
1195                $result = $this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->read($size, ($this->dnssec == true) ? $this->dnssec_payload_size : Net_DNS2_Lookups::DNS_MAX_UDP_SIZE);
1196                if ( ($result === false) || ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE) ) {
1197
1198                    //
1199                    // if we get an error, then keeping this socket around for a future request, could cause
1200                    // an error- for example, https://github.com/mikepultz/netdns2/issues/61
1201                    //
1202                    // in this case, the connection was timing out, which once it did finally respond, left
1203                    // data on the socket, which could be captured on a subsequent request.
1204                    //
1205                    // since there's no way to "reset" a socket, the only thing we can do it close it.
1206                    //
1207                    $this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1208                }
1209
1210                //
1211                // parse the first chunk as a packet
1212                //
1213                $chunk = new Net_DNS2_Packet_Response($result, $size);
1214
1215                //
1216                // if this is the first packet, then clone it directly, then
1217                // go through it to see if there are two SOA records
1218                // (indicating that it's the only packet)
1219                //
1220                if (is_null($response) == true) {
1221
1222                    $response = clone $chunk;
1223
1224                    //
1225                    // look for a failed response; if the zone transfer
1226                    // failed, then we don't need to do anything else at this
1227                    // point, and we should just break out.
1228                    //
1229                    if ($response->header->rcode != Net_DNS2_Lookups::RCODE_NOERROR) {
1230                        break;
1231                    }
1232
1233                    //
1234                    // go through each answer
1235                    //
1236                    foreach ($response->answer as $index => $rr) {
1237
1238                        //
1239                        // count the SOA records
1240                        //
1241                        if ($rr->type == 'SOA') {
1242                            $soa_count++;
1243                        }
1244                    }
1245
1246                    //
1247                    // if we have 2 or more SOA records, then we're done;
1248                    // otherwise continue out so we read the rest of the
1249                    // packets off the socket
1250                    //
1251                    if ($soa_count >= 2) {
1252                        break;
1253                    } else {
1254                        continue;
1255                    }
1256
1257                } else {
1258
1259                    //
1260                    // go through all these answers, and look for SOA records
1261                    //
1262                    foreach ($chunk->answer as $index => $rr) {
1263
1264                        //
1265                        // count the number of SOA records we find
1266                        //
1267                        if ($rr->type == 'SOA') {
1268                            $soa_count++;
1269                        }
1270
1271                        //
1272                        // add the records to a single response object
1273                        //
1274                        $response->answer[] = $rr;
1275                    }
1276
1277                    //
1278                    // if we've found the second SOA record, we're done
1279                    //
1280                    if ($soa_count >= 2) {
1281                        break;
1282                    }
1283                }
1284            }
1285
1286        //
1287        // everything other than a AXFR
1288        //
1289        } else {
1290
1291            $result = $this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->read($size, ($this->dnssec == true) ? $this->dnssec_payload_size : Net_DNS2_Lookups::DNS_MAX_UDP_SIZE);
1292            if ( ($result === false) || ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE) ) {
1293
1294                $this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1295            }
1296
1297            //
1298            // create the packet object
1299            //
1300            $response = new Net_DNS2_Packet_Response($result, $size);
1301        }
1302
1303        //
1304        // store the query time
1305        //
1306        $response->response_time = microtime(true) - $start_time;
1307
1308        //
1309        // add the name server that the response came from to the response object,
1310        // and the socket type that was used.
1311        //
1312        $response->answer_from = $_ns;
1313        $response->answer_socket_type = Net_DNS2_Socket::SOCK_STREAM;
1314
1315        //
1316        // return the Net_DNS2_Packet_Response object
1317        //
1318        return $response;
1319    }
1320
1321    /**
1322     * sends a DNS request using UDP
1323     *
1324     * @param string  $_ns   the name server to use for the request
1325     * @param string  $_data the raw DNS packet data
1326     *
1327     * @return Net_DNS2_Packet_Response the reponse object
1328     * @throws Net_DNS2_Exception
1329     * @access private
1330     *
1331     */
1332    private function sendUDPRequest($_ns, $_data)
1333    {
1334        //
1335        // grab the start time
1336        //
1337        $start_time = microtime(true);
1338
1339        //
1340        // see if we already have an open socket from a previous request; if so, try to use
1341        // that instead of opening a new one.
1342        //
1343        if ( (!isset($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]))
1344            || (!($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns] instanceof Net_DNS2_Socket))
1345        ) {
1346
1347            //
1348            // if the socket library is available, then use that
1349            //
1350            if ($this->sockets_enabled === true) {
1351
1352                $this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns] = new Net_DNS2_Socket_Sockets(
1353                    Net_DNS2_Socket::SOCK_DGRAM, $_ns, $this->dns_port, $this->timeout
1354                );
1355
1356            //
1357            // otherwise the streams library
1358            //
1359            } else {
1360
1361                $this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns] = new Net_DNS2_Socket_Streams(
1362                    Net_DNS2_Socket::SOCK_DGRAM, $_ns, $this->dns_port, $this->timeout
1363                );
1364            }
1365
1366            //
1367            // if a local IP address / port is set, then add it
1368            //
1369            if (strlen($this->local_host) > 0) {
1370
1371                $this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->bindAddress(
1372                    $this->local_host, $this->local_port
1373                );
1374            }
1375
1376            //
1377            // open the socket
1378            //
1379            if ($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->open() === false) {
1380
1381                $this->generateError(Net_DNS2_Socket::SOCK_DGRAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1382            }
1383        }
1384
1385        //
1386        // write the data to the socket
1387        //
1388        if ($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->write($_data) === false) {
1389
1390            $this->generateError(Net_DNS2_Socket::SOCK_DGRAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1391        }
1392
1393        //
1394        // read the content, using select to wait for a response
1395        //
1396        $size = 0;
1397
1398        $result = $this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->read($size, ($this->dnssec == true) ? $this->dnssec_payload_size : Net_DNS2_Lookups::DNS_MAX_UDP_SIZE);
1399        if (( $result === false) || ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE)) {
1400
1401            $this->generateError(Net_DNS2_Socket::SOCK_DGRAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1402        }
1403
1404        //
1405        // create the packet object
1406        //
1407        $response = new Net_DNS2_Packet_Response($result, $size);
1408
1409        //
1410        // store the query time
1411        //
1412        $response->response_time = microtime(true) - $start_time;
1413
1414        //
1415        // add the name server that the response came from to the response object,
1416        // and the socket type that was used.
1417        //
1418        $response->answer_from = $_ns;
1419        $response->answer_socket_type = Net_DNS2_Socket::SOCK_DGRAM;
1420
1421        //
1422        // return the Net_DNS2_Packet_Response object
1423        //
1424        return $response;
1425    }
1426}
1427
1428/*
1429 * Local variables:
1430 * tab-width: 4
1431 * c-basic-offset: 4
1432 * c-hanging-comment-ender-p: nil
1433 * End:
1434 */
1435?>
1436