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.4';
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 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 = '';
917
918        while (1) {
919
920            //
921            // grab the next DNS server
922            //
923            $ns = current($this->nameservers);
924            next($this->nameservers);
925
926            if ($ns === false) {
927
928                if (is_null($this->last_exception) == false) {
929
930                    throw $this->last_exception;
931                } else {
932
933                    throw new Net_DNS2_Exception(
934                        'every name server provided has failed',
935                        Net_DNS2_Lookups::E_NS_FAILED
936                    );
937                }
938            }
939
940            //
941            // if the use TCP flag (force TCP) is set, or the packet is bigger than our
942            // max allowed UDP size- which is either 512, or if this is DNSSEC request,
943            // then whatever the configured dnssec_payload_size is.
944            //
945            $max_udp_size = Net_DNS2_Lookups::DNS_MAX_UDP_SIZE;
946            if ($this->dnssec == true)
947            {
948                $max_udp_size = $this->dnssec_payload_size;
949            }
950
951            if ( ($use_tcp == true) || (strlen($data) > $max_udp_size) ) {
952
953                try
954                {
955                    $response = $this->sendTCPRequest($ns, $data, ($request->question[0]->qtype == 'AXFR') ? true : false);
956
957                } catch(Net_DNS2_Exception $e) {
958
959                    $this->last_exception = $e;
960                    $this->last_exception_list[$ns] = $e;
961
962                    continue;
963                }
964
965            //
966            // otherwise, send it using UDP
967            //
968            } else {
969
970                try
971                {
972                    $response = $this->sendUDPRequest($ns, $data);
973
974                    //
975                    // check the packet header for a trucated bit; if it was truncated,
976                    // then re-send the request as TCP.
977                    //
978                    if ($response->header->tc == 1) {
979
980                        $response = $this->sendTCPRequest($ns, $data);
981                    }
982
983                } catch(Net_DNS2_Exception $e) {
984
985                    $this->last_exception = $e;
986                    $this->last_exception_list[$ns] = $e;
987
988                    continue;
989                }
990            }
991
992            //
993            // make sure header id's match between the request and response
994            //
995            if ($request->header->id != $response->header->id) {
996
997                $this->last_exception = new Net_DNS2_Exception(
998
999                    'invalid header: the request and response id do not match.',
1000                    Net_DNS2_Lookups::E_HEADER_INVALID,
1001                    null,
1002                    $request,
1003                    $response
1004                );
1005
1006                $this->last_exception_list[$ns] = $this->last_exception;
1007                continue;
1008            }
1009
1010            //
1011            // make sure the response is actually a response
1012            //
1013            // 0 = query, 1 = response
1014            //
1015            if ($response->header->qr != Net_DNS2_Lookups::QR_RESPONSE) {
1016
1017                $this->last_exception = new Net_DNS2_Exception(
1018
1019                    'invalid header: the response provided is not a response packet.',
1020                    Net_DNS2_Lookups::E_HEADER_INVALID,
1021                    null,
1022                    $request,
1023                    $response
1024                );
1025
1026                $this->last_exception_list[$ns] = $this->last_exception;
1027                continue;
1028            }
1029
1030            //
1031            // make sure the response code in the header is ok
1032            //
1033            if ($response->header->rcode != Net_DNS2_Lookups::RCODE_NOERROR) {
1034
1035                $this->last_exception = new Net_DNS2_Exception(
1036
1037                    'DNS request failed: ' .
1038                    Net_DNS2_Lookups::$result_code_messages[$response->header->rcode],
1039                    $response->header->rcode,
1040                    null,
1041                    $request,
1042                    $response
1043                );
1044
1045                $this->last_exception_list[$ns] = $this->last_exception;
1046                continue;
1047            }
1048
1049            break;
1050        }
1051
1052        return $response;
1053    }
1054
1055    /**
1056     * cleans up a failed socket and throws the given exception
1057     *
1058     * @param string  $_proto the protocol of the socket
1059     * @param string  $_ns    the name server to use for the request
1060     * @param string  $_error the error message to throw at the end of the function
1061     *
1062     * @throws Net_DNS2_Exception
1063     * @access private
1064     *
1065     */
1066    private function generateError($_proto, $_ns, $_error)
1067    {
1068        if (isset($this->sock[$_proto][$_ns]) == false)
1069        {
1070            throw new Net_DNS2_Exception('invalid socket referenced', Net_DNS2_Lookups::E_NS_INVALID_SOCKET);
1071        }
1072
1073        //
1074        // grab the last error message off the socket
1075        //
1076        $last_error = $this->sock[$_proto][$_ns]->last_error;
1077
1078        //
1079        // close it
1080        //
1081        $this->sock[$_proto][$_ns]->close();
1082
1083        //
1084        // remove it from the socket cache
1085        //
1086        unset($this->sock[$_proto][$_ns]);
1087
1088        //
1089        // throw the error provided
1090        //
1091        throw new Net_DNS2_Exception($last_error, $_error);
1092    }
1093
1094    /**
1095     * sends a DNS request using TCP
1096     *
1097     * @param string  $_ns   the name server to use for the request
1098     * @param string  $_data the raw DNS packet data
1099     * @param boolean $_axfr if this is a zone transfer request
1100     *
1101     * @return Net_DNS2_Packet_Response the reponse object
1102     * @throws Net_DNS2_Exception
1103     * @access private
1104     *
1105     */
1106    private function sendTCPRequest($_ns, $_data, $_axfr = false)
1107    {
1108        //
1109        // grab the start time
1110        //
1111        $start_time = microtime(true);
1112
1113        //
1114        // see if we already have an open socket from a previous request; if so, try to use
1115        // that instead of opening a new one.
1116        //
1117        if ( (!isset($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]))
1118            || (!($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns] instanceof Net_DNS2_Socket))
1119        ) {
1120
1121            //
1122            // if the socket library is available, then use that
1123            //
1124            if ($this->sockets_enabled === true) {
1125
1126                $this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns] = new Net_DNS2_Socket_Sockets(
1127                    Net_DNS2_Socket::SOCK_STREAM, $_ns, $this->dns_port, $this->timeout
1128                );
1129
1130            //
1131            // otherwise the streams library
1132            //
1133            } else {
1134
1135                $this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns] = new Net_DNS2_Socket_Streams(
1136                    Net_DNS2_Socket::SOCK_STREAM, $_ns, $this->dns_port, $this->timeout
1137                );
1138            }
1139
1140            //
1141            // if a local IP address / port is set, then add it
1142            //
1143            if (strlen($this->local_host) > 0) {
1144
1145                $this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->bindAddress(
1146                    $this->local_host, $this->local_port
1147                );
1148            }
1149
1150            //
1151            // open the socket
1152            //
1153            if ($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->open() === false) {
1154
1155                $this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1156            }
1157        }
1158
1159        //
1160        // write the data to the socket; if it fails, continue on
1161        // the while loop
1162        //
1163        if ($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->write($_data) === false) {
1164
1165            $this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1166        }
1167
1168        //
1169        // read the content, using select to wait for a response
1170        //
1171        $size = 0;
1172        $result = null;
1173        $response = null;
1174
1175        //
1176        // handle zone transfer requests differently than other requests.
1177        //
1178        if ($_axfr == true) {
1179
1180            $soa_count = 0;
1181
1182            while (1) {
1183
1184                //
1185                // read the data off the socket
1186                //
1187                $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);
1188                if ( ($result === false) || ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE) ) {
1189
1190                    //
1191                    // if we get an error, then keeping this socket around for a future request, could cause
1192                    // an error- for example, https://github.com/mikepultz/netdns2/issues/61
1193                    //
1194                    // in this case, the connection was timing out, which once it did finally respond, left
1195                    // data on the socket, which could be captured on a subsequent request.
1196                    //
1197                    // since there's no way to "reset" a socket, the only thing we can do it close it.
1198                    //
1199                    $this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1200                }
1201
1202                //
1203                // parse the first chunk as a packet
1204                //
1205                $chunk = new Net_DNS2_Packet_Response($result, $size);
1206
1207                //
1208                // if this is the first packet, then clone it directly, then
1209                // go through it to see if there are two SOA records
1210                // (indicating that it's the only packet)
1211                //
1212                if (is_null($response) == true) {
1213
1214                    $response = clone $chunk;
1215
1216                    //
1217                    // look for a failed response; if the zone transfer
1218                    // failed, then we don't need to do anything else at this
1219                    // point, and we should just break out.
1220                    //
1221                    if ($response->header->rcode != Net_DNS2_Lookups::RCODE_NOERROR) {
1222                        break;
1223                    }
1224
1225                    //
1226                    // go through each answer
1227                    //
1228                    foreach ($response->answer as $index => $rr) {
1229
1230                        //
1231                        // count the SOA records
1232                        //
1233                        if ($rr->type == 'SOA') {
1234                            $soa_count++;
1235                        }
1236                    }
1237
1238                    //
1239                    // if we have 2 or more SOA records, then we're done;
1240                    // otherwise continue out so we read the rest of the
1241                    // packets off the socket
1242                    //
1243                    if ($soa_count >= 2) {
1244                        break;
1245                    } else {
1246                        continue;
1247                    }
1248
1249                } else {
1250
1251                    //
1252                    // go through all these answers, and look for SOA records
1253                    //
1254                    foreach ($chunk->answer as $index => $rr) {
1255
1256                        //
1257                        // count the number of SOA records we find
1258                        //
1259                        if ($rr->type == 'SOA') {
1260                            $soa_count++;
1261                        }
1262
1263                        //
1264                        // add the records to a single response object
1265                        //
1266                        $response->answer[] = $rr;
1267                    }
1268
1269                    //
1270                    // if we've found the second SOA record, we're done
1271                    //
1272                    if ($soa_count >= 2) {
1273                        break;
1274                    }
1275                }
1276            }
1277
1278        //
1279        // everything other than a AXFR
1280        //
1281        } else {
1282
1283            $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);
1284            if ( ($result === false) || ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE) ) {
1285
1286                $this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1287            }
1288
1289            //
1290            // create the packet object
1291            //
1292            $response = new Net_DNS2_Packet_Response($result, $size);
1293        }
1294
1295        //
1296        // store the query time
1297        //
1298        $response->response_time = microtime(true) - $start_time;
1299
1300        //
1301        // add the name server that the response came from to the response object,
1302        // and the socket type that was used.
1303        //
1304        $response->answer_from = $_ns;
1305        $response->answer_socket_type = Net_DNS2_Socket::SOCK_STREAM;
1306
1307        //
1308        // return the Net_DNS2_Packet_Response object
1309        //
1310        return $response;
1311    }
1312
1313    /**
1314     * sends a DNS request using UDP
1315     *
1316     * @param string  $_ns   the name server to use for the request
1317     * @param string  $_data the raw DNS packet data
1318     *
1319     * @return Net_DNS2_Packet_Response the reponse object
1320     * @throws Net_DNS2_Exception
1321     * @access private
1322     *
1323     */
1324    private function sendUDPRequest($_ns, $_data)
1325    {
1326        //
1327        // grab the start time
1328        //
1329        $start_time = microtime(true);
1330
1331        //
1332        // see if we already have an open socket from a previous request; if so, try to use
1333        // that instead of opening a new one.
1334        //
1335        if ( (!isset($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]))
1336            || (!($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns] instanceof Net_DNS2_Socket))
1337        ) {
1338
1339            //
1340            // if the socket library is available, then use that
1341            //
1342            if ($this->sockets_enabled === true) {
1343
1344                $this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns] = new Net_DNS2_Socket_Sockets(
1345                    Net_DNS2_Socket::SOCK_DGRAM, $_ns, $this->dns_port, $this->timeout
1346                );
1347
1348            //
1349            // otherwise the streams library
1350            //
1351            } else {
1352
1353                $this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns] = new Net_DNS2_Socket_Streams(
1354                    Net_DNS2_Socket::SOCK_DGRAM, $_ns, $this->dns_port, $this->timeout
1355                );
1356            }
1357
1358            //
1359            // if a local IP address / port is set, then add it
1360            //
1361            if (strlen($this->local_host) > 0) {
1362
1363                $this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->bindAddress(
1364                    $this->local_host, $this->local_port
1365                );
1366            }
1367
1368            //
1369            // open the socket
1370            //
1371            if ($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->open() === false) {
1372
1373                $this->generateError(Net_DNS2_Socket::SOCK_DGRAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1374            }
1375        }
1376
1377        //
1378        // write the data to the socket
1379        //
1380        if ($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->write($_data) === false) {
1381
1382            $this->generateError(Net_DNS2_Socket::SOCK_DGRAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1383        }
1384
1385        //
1386        // read the content, using select to wait for a response
1387        //
1388        $size = 0;
1389
1390        $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);
1391        if (( $result === false) || ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE)) {
1392
1393            $this->generateError(Net_DNS2_Socket::SOCK_DGRAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
1394        }
1395
1396        //
1397        // create the packet object
1398        //
1399        $response = new Net_DNS2_Packet_Response($result, $size);
1400
1401        //
1402        // store the query time
1403        //
1404        $response->response_time = microtime(true) - $start_time;
1405
1406        //
1407        // add the name server that the response came from to the response object,
1408        // and the socket type that was used.
1409        //
1410        $response->answer_from = $_ns;
1411        $response->answer_socket_type = Net_DNS2_Socket::SOCK_DGRAM;
1412
1413        //
1414        // return the Net_DNS2_Packet_Response object
1415        //
1416        return $response;
1417    }
1418}
1419
1420/*
1421 * Local variables:
1422 * tab-width: 4
1423 * c-basic-offset: 4
1424 * c-hanging-comment-ender-p: nil
1425 * End:
1426 */
1427?>
1428