<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * DNS Library for handling lookups and updates. 
 *
 * PHP Version 5
 *
 * Copyright (c) 2010, Mike Pultz <mike@mikepultz.com>.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *
 *   * Neither the name of Mike Pultz nor the names of his contributors 
 *     may be used to endorse or promote products derived from this 
 *     software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRIC
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * @category  Networking
 * @package   Net_DNS2
 * @author    Mike Pultz <mike@mikepultz.com>
 * @copyright 2010 Mike Pultz <mike@mikepultz.com>
 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License
 * @version   SVN: $Id: DNS2.php 137 2011-12-05 02:51:34Z mike.pultz $
 * @link      http://pear.php.net/package/Net_DNS2
 * @since     File available since Release 0.6.0
 *
 */

/*
 * register the auto-load function
 *
 */
spl_autoload_register('Net_DNS2::autoload');

/**
 * This is the base class for the Net_DNS2_Resolver and Net_DNS2_Updater
 * classes.
 *
 * @category Networking
 * @package  Net_DNS2
 * @author   Mike Pultz <mike@mikepultz.com>
 * @license  http://www.opensource.org/licenses/bsd-license.php  BSD License
 * @link     http://pear.php.net/package/Net_DNS2
 * @see      Net_DNS2_Resolver, Net_DNS2_Updater
 *
 */
class Net_DNS2
{
    /*
     * the current version of this library
     */
    const VERSION = '1.2.0';

    /*
     * the default path to a resolv.conf file
     */
    const RESOLV_CONF = '/etc/resolv.conf';

    /*
     * use TCP only (true/false)
     */
    public $use_tcp = false;

    /*
     * DNS Port to use (53)
     */
    public $dns_port = 53;

    /*
     * the ip/port for use as a local socket
     */
    public $local_host = '';
    public $local_port = 0;

    /*
     * timeout value for socket connections
     */
    public $timeout = 5;

    /*
     * randomize the name servers list
     */
    public $ns_random = false;

    /*
     * default domains
     */
    public $domain = '';

    /*
     * domain search list - not actually used right now
     */
    public $search_list = array();

    /*
     * enable cache; either "shared", "file" or "none"
     */
    public $cache_type = 'none';

    /*
     * file name to use for shared memory segment or file cache
     */
    public $cache_file = '/tmp/net_dns2.cache';

    /*
     * the max size of the cache file (in bytes)
     */
    public $cache_size = 10000;

    /*
     * the method to use for storing cache data; either "serialize" or "json"
     *
     * json is faster, but can't remember the class names (everything comes back as a
     * "stdClass Object"; all the data is the same though. serialize is slower, but will
     * have all the class info.
     *
     * defaults to 'serialize'
     */
    public $cache_serializer = 'serialize';

    /*
     * local sockets
     */
    protected $sock = array('udp' => array(), 'tcp' => array());

    /*
     * name server list
     */
    protected $nameservers = array();

    /*
     * if the socket extension is loaded
     */
    protected $sockets_enabled = false;

    /*
     * the TSIG or SIG RR object for authentication
     */
    protected $auth_signature = null;

    /*
     * the shared memory segment id for the local cache
     */
    protected $cache = null;

    /*
     * internal setting for enabling cache
     */
    protected $use_cache = false;

    /*
     * the last erro message returned by the sockets class
     */
    private $_last_socket_error = '';

    /**
     * Constructor - base constructor for the Resolver and Updater
     *
     * @param mixed $options array of options or null for none
     *
     * @throws Net_DNS2_Exception
     * @access public
     *
     */
    public function __construct(array $options = null)
    {
        //
        // check for the sockets extension
        //
        $this->sockets_enabled = extension_loaded('sockets');

        //
        // load any options that were provided
        //
        if (!empty($options)) {

            foreach ($options as $key => $value) {

                if ($key == 'nameservers') {

                    $this->setServers($value);
                } else {

                    $this->$key = $value;
                }
            }
        }

        //
        // if we're set to use the local shared memory cache, then
        // make sure it's been initialized
        //
        switch($this->cache_type) {
        case 'shared':
            if (extension_loaded('shmop')) {

                $this->cache = new Net_DNS2_Cache_Shm;
                $this->use_cache = true;
            } else {

                throw new Net_DNS2_Exception(
                    'shmop library is not available for cache',
                    Net_DNS2_Lookups::E_CACHE_SHM_UNAVAIL
                );
            }
            break;
        case 'file':

            $this->cache = new Net_DNS2_Cache_File;
            $this->use_cache = true;

            break;  
        case 'none':
            $this->use_cache = false;
            break;
        default:

            throw new Net_DNS2_Exception(
                'un-supported cache type: ' . $this->cache_type,
                Net_DNS2_Lookups::E_CACHE_UNSUPPORTED
            );
        }
    }

    /**
     * autoload call-back function; used to auto-load classes
     *
     * @param string $name the name of the class
     *
     * @return void
     * @access public
     *
     */
    static public function autoload($name)
    {
        //
        // only auto-load our classes
        //
        if (strncmp($name, 'Net_DNS2', 8) == 0) {

            include str_replace('_', '/', $name) . '.php';
        }

        return;
    }

    /**
     * sets the name servers to be used
     *
     * @param mixed $nameservers either an array of name servers, or a file name 
     *                           to parse, assuming it's in the resolv.conf format
     *
     * @return boolean
     * @throws Net_DNS2_Exception
     * @access public
     *
     */
    public function setServers($nameservers)
    {
        //
        // if it's an array, then use it directly
        //
        // otherwise, see if it's a path to a resolv.conf file and if so, load it
        //
        if (is_array($nameservers)) {

            $this->nameservers = $nameservers;

        } else {

            //
            // check to see if the file is readable
            //
            if (is_readable($nameservers) === true) {
    
                $data = file_get_contents($nameservers);
                if ($data === false) {
                    throw new Net_DNS2_Exception(
                        'failed to read contents of file: ' . $nameservers,
                        Net_DNS2_Lookups::E_NS_INVALID_FILE
                    );
                }

                $lines = explode("\n", $data);

                foreach ($lines as $line) {
                    
                    $line = trim($line);

                    //
                    // ignore empty lines, and lines that are commented out
                    //
                    if ( (strlen($line) == 0) 
                        || ($line[0] == '#') 
                        || ($line[0] == ';')
                    ) {
                        continue;
                    }

                    list($key, $value) = preg_split('/\s+/', $line, 2);

                    $key    = trim(strtolower($key));
                    $value  = trim(strtolower($value));

                    switch($key) {
                    case 'nameserver':

                        //
                        // nameserver can be a IPv4 or IPv6 address
                        //
                        if ( (self::isIPv4($value) == true) 
                            || (self::isIPv6($value) == true)
                        ) {

                            $this->nameservers[] = $value;
                        } else {

                            throw new Net_DNS2_Exception(
                                'invalid nameserver entry: ' . $value,
                                Net_DNS2_Lookups::E_NS_INVALID_ENTRY
                            );
                        }
                        break;

                    case 'domain':
                        $this->domain = $value;
                        break;

                    case 'search':
                        $this->search_list = preg_split('/\s+/', $value);
                        break;

                    default:
                        ;
                    }
                }

                //
                // if we don't have a domain, but we have a search list, then
                // take the first entry on the search list as the domain
                //
                if ( (strlen($this->domain) == 0) 
                    && (count($this->search_list) > 0) 
                ) {
                    $this->domain = $this->search_list[0];
                }

            } else {
                throw new Net_DNS2_Exception(
                    'resolver file file provided is not readable: ' . $nameservers,
                    Net_DNS2_Lookups::E_NS_INVALID_FILE
                );
            }
        }

        //
        // check the name servers
        //
        $this->checkServers();

        return true;
    }

    /**
     * checks the list of name servers to make sure they're set
     *
     * @param mixed $default a path to a resolv.conf file or an array of servers.
     *
     * @return boolean
     * @throws Net_DNS2_Exception
     * @access protected
     *
     */
    protected function checkServers($default = null)
    {
        if (empty($this->nameservers)) {

            if (isset($default)) {

                $this->setServers($default);
            } else {

                throw new Net_DNS2_Exception(
                    'empty name servers list; you must provide a list of name '.
                    'servers, or the path to a resolv.conf file.',
                    Net_DNS2_Lookups::E_NS_INVALID_ENTRY
                );
            }
        }
    
        return true;
    }

    /**
     * adds a TSIG RR object for authentication
     *
     * @param string $keyname   the key name to use for the TSIG RR
     * @param string $signature the key to sign the request.
     * 
     * @return boolean
     * @access public
     * @since  function available since release 1.1.0
     *
     */
    public function signTSIG($keyname, $signature = '')
    {
        //
        // if the TSIG was pre-created and passed in, then we can just used 
        // it as provided.
        //
        if ($keyname instanceof Net_DNS2_RR_TSIG) {

            $this->auth_signature = $keyname;

        } else {

            //
            // otherwise create the TSIG RR, but don't add it just yet; TSIG needs 
            // to be added as the last additional entry- so we'll add it just 
            // before we send.
            //
            $this->auth_signature = Net_DNS2_RR::fromString(
                strtolower(trim($keyname)) .
                ' TSIG '. $signature
            );
        }
          
        return true;
    }

    /**
     * adds a SIG RR object for authentication
     *
     * @param string $filename the name of a file to load the signature from.
     * 
     * @return boolean
     * @throws Net_DNS2_Exception
     * @access public
     * @since  function available since release 1.1.0
     *
     */
    public function signSIG0($filename)
    {
        //
        // check for OpenSSL
        //
        if (extension_loaded('openssl') === false) {
            
            throw new Net_DNS2_Exception(
                'the OpenSSL extension is required to use SIG(0).',
                Net_DNS2_Lookups::E_OPENSSL_UNAVAIL
            );
        }

        //
        // if the SIG was pre-created, then use it as-is
        //
        if ($filename instanceof Net_DNS2_RR_SIG) {

            $this->auth_signature = $filename;

        } else {
        
            //
            // otherwise, it's filename which needs to be parsed and processed.
            //
            $private = new Net_DNS2_PrivateKey($filename);

            //
            // create a new Net_DNS2_RR_SIG object
            //
            $this->auth_signature = new Net_DNS2_RR_SIG();

            //
            // reset some values
            //
            $this->auth_signature->name         = $private->signname;
            $this->auth_signature->ttl          = 0;
            $this->auth_signature->class        = 'ANY';

            //
            // these values are pulled from the private key
            //
            $this->auth_signature->algorithm    = $private->algorithm;
            $this->auth_signature->keytag       = $private->keytag;
            $this->auth_signature->signname     = $private->signname;

            //
            // these values are hard-coded for SIG0
            //
            $this->auth_signature->typecovered  = 'SIG0';
            $this->auth_signature->labels       = 0;
            $this->auth_signature->origttl      = 0;

            //
            // generate the dates
            //
            $t = time();

            $this->auth_signature->sigincep     = gmdate('YmdHis', $t);
            $this->auth_signature->sigexp       = gmdate('YmdHis', $t + 500);

            //
            // store the private key in the SIG object for later.
            //
            $this->auth_signature->private_key  = $private;
        }

        //
        // only RSAMD5 and RSASHA1 are supported for SIG(0)
        //
        switch($this->auth_signature->algorithm) {
        case Net_DNS2_Lookups::DNSSEC_ALGORITHM_RSAMD5:
        case Net_DNS2_Lookups::DNSSEC_ALGORITHM_RSASHA1:
            break;
        default:
            throw new Net_DNS2_Exception(
                'only asymmetric algorithms work with SIG(0)!',
                Net_DNS2_Lookups::E_OPENSSL_INV_ALGO
            );
        }

        return true;
    }

    /**
     * returns true/false if the given address is a valid IPv4 address
     *
     * @param string $_address the IPv4 address to check
     *
     * @return boolean returns true/false if the address is IPv4 address
     * @access public
     *
     */
    public static function isIPv4($_address)
    {
        //
        // use filter_var() if it's available; it's faster than preg
        //
        if (extension_loaded('filter') == true) {

            if (filter_var(
                $_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4
            ) == false) {
                return false;
            }
        } else {

            //
            // do the main check here;
            //
            if (inet_pton($_address) === false) {
                return false;
            }

            //
            // then make sure we're not a IPv6 address
            //
            if (preg_match(
                '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', 
                $_address
            ) == 0) {
                return false;
            }
        }

        return true;
    }
    
    /**
     * returns true/false if the given address is a valid IPv6 address
     *
     * @param string $_address the IPv6 address to check
     *
     * @return boolean returns true/false if the address is IPv6 address
     * @access public
     *
     */
    public static function isIPv6($_address)
    {
        //
        // use filter_var() if it's available; it's faster than preg
        //
        if (extension_loaded('filter') == true) {
            if (filter_var(
                $_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6
            ) == false) {
                return false;
            }
        } else {

            //
            // do the main check here
            //
            if (inet_pton($_address) === false) {
                return false;
            }

            //
            // then make sure it doesn't match a IPv4 address
            //
            if (preg_match(
                '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $_address
            ) == 1) {
                return false;
            }
        }

        return true;
    }

    /**
     * formats the given IPv6 address as a fully expanded IPv6 address
     *
     * @param string $_address the IPv6 address to expand
     *
     * @return string the fully expanded IPv6 address
     * @access public
     *
     */
    public static function expandIPv6($_address)
    {
        if (strpos($_address, '::') !== false) {
        
            $part       = explode('::', $_address);
            $part[0]    = explode(':', $part[0]);
            $part[1]    = explode(':', $part[1]);
        
            $missing = array();
        
            $x = (8 - (count($part[0]) + count($part[1])));
            for ($i = 0; $i < $x; $i++) {
            
                array_push($missing, '0000');
            }

            $missing    = array_merge($part[0], $missing);
            $part       = array_merge($missing, $part[1]);

        } else {
        
            $part = explode(':', $_address);
        }

        foreach ($part as &$p) {
            while (strlen($p) < 4) {
                $p = '0' . $p;
            }
        }

        unset($p);
    
        $result = implode(':', $part);

        if (strlen($result) == 39) {
            return $result;
        } else {
            return false;
        }
    }

    /**
     * sends a standard Net_DNS2_Packet_Request packet
     *
     * @param Net_DNS2_Packet $request a Net_DNS2_Packet_Request object
     * @param boolean         $use_tcp true/false if the function should
     *                                 use TCP for the request
     *
     * @return mixed returns a Net_DNS2_Packet_Response object, or false on error
     * @throws Net_DNS2_Exception
     * @access protected
     *
     */
    protected function sendPacket(Net_DNS2_Packet $request, $use_tcp)
    {
        //
        // get the data from the packet
        //
        $data = $request->get();
        if (strlen($data) < Net_DNS2_Lookups::DNS_HEADER_SIZE) {

            throw new Net_DNS2_Exception(
                'invalid or empty packet for sending!',
                Net_DNS2_Lookups::E_PACKET_INVALID
            );
        }

        reset($this->nameservers);
        
        //
        // randomize the name server list if it's asked for
        //
        if ($this->ns_random == true) {

            shuffle($this->nameservers);
        }

        //
        // loop so we can handle server errors
        //
        $response = null;
        $ns = '';
        $tcp_fallback = false;

        while (1) {

            //
            // grab the next DNS server
            //
            if ($tcp_fallback == false) {

                $ns = each($this->nameservers);
                if ($ns === false) {

                    throw new Net_DNS2_Exception(
                        'every name server provided has failed: ' . 
                        $this->_last_socket_error,
                        Net_DNS2_Lookups::E_NS_FAILED
                    );
                }

                $ns = $ns[1];
            }

            //
            // if the use TCP flag (force TCP) is set, or the packet is bigger 
            // than 512 bytes, use TCP for sending the packet
            //
            if ( ($use_tcp == true) 
                || (strlen($data) > Net_DNS2_Lookups::DNS_MAX_UDP_SIZE) 
                || ($tcp_fallback == true)
            ) {
                $tcp_fallback = false;

                //
                // create the socket object
                //
                if ( (!isset($this->sock['tcp'][$ns])) 
                    || (!($this->sock['tcp'][$ns] instanceof Net_DNS2_Socket)) 
                ) {
                    if ($this->sockets_enabled === true) {

                        $this->sock['tcp'][$ns] = new Net_DNS2_Socket_Sockets(
                            SOCK_STREAM, $ns, $this->dns_port, $this->timeout
                        );
                    } else {

                        $this->sock['tcp'][$ns] = new Net_DNS2_Socket_Streams(
                            SOCK_STREAM, $ns, $this->dns_port, $this->timeout
                        );
                    }
                }

                //
                // if a local IP address / port is set, then add it
                //
                if (strlen($this->local_host) > 0) {

                    $this->sock['tcp'][$ns]->bindAddress(
                        $this->local_host, $this->local_port
                    );
                }

                //
                // open it; if it fails, continue in the while loop
                //
                if ($this->sock['tcp'][$ns]->open() === false) {

                    $this->_last_socket_error = $this->sock['tcp'][$ns]->last_error;
                    continue;
                }

                //
                // write the data to the socket; if it fails, continue on 
                // the while loop
                //
                if ($this->sock['tcp'][$ns]->write($data) === false) {

                    $this->_last_socket_error = $this->sock['tcp'][$ns]->last_error;
                    continue;
                }

                //
                // read the content, using select to wait for a response
                //
                $size = 0;

                $result = $this->sock['tcp'][$ns]->read($size);
                if ( ($result === false) 
                    ||  ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE) 
                ) {
                    $this->_last_socket_error = $this->sock['tcp'][$ns]->last_error;
                    continue;
                }

                //
                // create the packet object
                //
                $response = new Net_DNS2_Packet_Response($result, $size);
                break;

            } else {

                //
                // create the socket object
                //
                if ( (!isset($this->sock['udp'][$ns])) 
                    || (!($this->sock['udp'][$ns] instanceof Net_DNS2_Socket))
                ) {
                    if ($this->sockets_enabled === true) {

                        $this->sock['udp'][$ns] = new Net_DNS2_Socket_Sockets(
                            SOCK_DGRAM, $ns, $this->dns_port, $this->timeout
                        );
                    } else {

                        $this->sock['udp'][$ns] = new Net_DNS2_Socket_Streams(
                            SOCK_DGRAM, $ns, $this->dns_port, $this->timeout
                        );
                    }
                }            

                //
                // if a local IP address / port is set, then add it
                //
                if (strlen($this->local_host) > 0) {

                    $this->sock['udp'][$ns]->bindAddress(
                        $this->local_host, $this->local_port
                    );
                }

                //
                // open it
                //
                if ($this->sock['udp'][$ns]->open() === false) {

                    $this->_last_socket_error = $this->sock['udp'][$ns]->last_error;
                    continue;
                }

                //
                // write the data to the socket
                //
                if ($this->sock['udp'][$ns]->write($data) === false) {

                    $this->_last_socket_error = $this->sock['udp'][$ns]->last_error;
                    continue;
                }

                //
                // read the content, using select to wait for a response
                //
                $size = 0;

                $result = $this->sock['udp'][$ns]->read($size);
                if (( $result === false) 
                    || ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE)
                ) {
                    $this->_last_socket_error = $this->sock['udp'][$ns]->last_error;
                    continue;
                }

                //
                // create the packet object
                //
                $response = new Net_DNS2_Packet_Response($result, $size);

                //
                // check the packet header for a trucated bit; if it was truncated,
                // then re-send the request as TCP.
                //
                if ($response->header->tc == 1) {

                    $tcp_fallback = true;
                    continue;
                }

                break;
            }
        }

        if (is_null($response)) {

            return false;
        }

        //
        // make sure header id's match between the request and response
        //
        if ($request->header->id != $response->header->id) {

            throw new Net_DNS2_Exception(
                'invalid header: the request and response id do not match.',
                Net_DNS2_Lookups::E_HEADER_INVALID
            );
        }

        //
        // make sure the response is actually a response
        // 
        // 0 = query, 1 = response
        //
        if ($response->header->qr != Net_DNS2_Lookups::QR_RESPONSE) {

            throw new Net_DNS2_Exception(
                'invalid header: the response provided is not a response packet.',
                Net_DNS2_Lookups::E_HEADER_INVALID
            );
        }

        //
        // make sure the response code in the header is ok
        //
        if ($response->header->rcode != Net_DNS2_Lookups::RCODE_NOERROR) {

            throw new Net_DNS2_Exception(
                'DNS request failed: ' . 
                Net_DNS2_Lookups::$result_code_messages[$response->header->rcode],
                $response->header->rcode
            );
        }

        return $response;
    }
}

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * c-hanging-comment-ender-p: nil
 * End:
 */
?>
