<?php

/* vim: set expandtab tabstop=4 shiftwidth=4: */

/**
 * Net_Traceroute
 *
 * Package for handling traceroute outputs
 *
 * This package is an interface to the traceroute/tracert-tool most
 * current OS offer. It parses the output into an easy-to-use array structure.
 * Please note: The parsing is OS-dependepent! So if your OS is currently
 * not supported, please let us know.
 * (credit: Idea and API originally based upon package Net_Ping.)
 *
 * PHP versions 4 and 5
 *
 * LICENSE: This source file is subject to version 2.0 of the PHP license,
 * that is bundled with this package in the file LICENSE, and is
 * available at through the world-wide-web at
 * http://www.php.net/license/2_02.txt.
 * If you did not receive a copy of the PHP license and are unable to
 * obtain it through the world-wide-web, please send a note to
 * license@php.net so we can mail you a copy immediately.
 *
 * @category  Net
 * @package   Net_Traceroute
 * @author    Stefan Neufeind <pear.neufeind@speedpartner.de>
 * @copyright 2003-2009 The PHP Group
 * @license   http://www.php.net/license/2_02.txt  PHP License 2.02
 * @version   SVN: $Id: Traceroute.php 304541 2010-10-20 07:46:28Z neufeind $
 * @link      http://pear.php.net/package/Net_Traceroute
 */

/**
* PEAR-base-class, needed for error-handling
*/
require_once "PEAR.php";

/**
* Class for determining the OS
*
* Knowning the correct OS is really essential to choose the right parser.
*/
require_once "OS/Guess.php";

define('NET_TRACEROUTE_FAILED_MSG', 'execution of traceroute failed');
define('NET_TRACEROUTE_HOST_NOT_FOUND_MSG', 'unknown host');
define('NET_TRACEROUTE_INVALID_ARGUMENTS_MSG', 'invalid argument array');
define('NET_TRACEROUTE_CANT_LOCATE_TRACEROUTE_BINARY_MSG', 'unable to locate the traceroute binary');
define('NET_TRACEROUTE_RESULT_UNSUPPORTED_BACKEND_MSG', 'Backend not Supported');

define('NET_TRACEROUTE_FAILED', 0);
define('NET_TRACEROUTE_HOST_NOT_FOUND', 1);
define('NET_TRACEROUTE_INVALID_ARGUMENTS', 2);
define('NET_TRACEROUTE_CANT_LOCATE_TRACEROUTE_BINARY', 3);
define('NET_TRACEROUTE_RESULT_UNSUPPORTED_BACKEND', 4);

/**
 * Wrapper class for traceroute calls
 *
 * @category  Net
 * @package   Net_Traceroute
 * @author    Stefan Neufeind <pear.neufeind@speedpartner.de>
 * @copyright 2003-2009 The PHP Group
 * @license   http://www.php.net/license/2_02.txt  PHP License 2.02
 * @version   Release: @package_version@
 * @link      http://pear.php.net/package/Net_Traceroute
 */
class Net_Traceroute
{
    /**
     * Location where the traceroute program is stored
     *
     * @var    string
     * @access private
     */
    var $_traceroute_path = "";

    /**
     * Array with the result from the traceroute execution
     *
     * @var    array
     * @access private
     */
    var $_result = array();

    /**
     * OS_Guess->getSysname result
     *
     * @var    string
     * @access private
     */
    var $_sysname = "";

    /**
     * Traceroute command arguments
     *
     * @var    array
     * @access private
     */
    var $_args = array();

    /**
     * Indicates if an empty array was given to setArgs (not used yet)
     *
     * @var    boolean
     * @access private
     */
    var $_noArgs = true;

    /**
     * Contains the argument->option relation
     *
     * @var    array
     * @access private
     */
    var $_argRelation = array();

    /**
     * Constructor
     *
     * @param string $traceroute_path Path to traceroute-binary
     * @param string $sysname         Systemname which identifies the OS
     *
     * @access private
     */
    function Net_Traceroute($traceroute_path, $sysname)
    {
        $this->_traceroute_path = $traceroute_path;
        $this->_sysname   = $sysname;
        $this->_initArgRelation();
    }

    /**
     * Factory for Net_Traceroute
     *
     * Call this method to create a new instance of Net_Traceroute
     *
     * @return object Net_Traceroute
     * @access public
     */
    function factory()
    {
        $OS_Guess  = new OS_Guess;
        $sysname   = $OS_Guess->getSysname();
        $traceroute_path = '';

        if (($traceroute_path = Net_Traceroute::_setTraceroutePath($sysname)) == NET_TRACEROUTE_CANT_LOCATE_TRACEROUTE_BINARY) {
            return PEAR::throwError(NET_TRACEROUTE_CANT_LOCATE_TRACEROUTE_BINARY_MSG, NET_TRACEROUTE_CANT_LOCATE_TRACEROUTE_BINARY);
        } else {
            return new Net_Traceroute($traceroute_path, $sysname);
        }

    }
    /**
     * Set the arguments array
     *
     * @param array $args Hash with options
     *
     * @return mixed true or PEAR_error
     * @access public
     */
    function setArgs($args)
    {
        if (!is_array($args)) {
            return PEAR::throwError(NET_TRACEROUTE_INVALID_ARGUMENTS_MSG, NET_TRACEROUTE_INVALID_ARGUMENTS);
        }

        /* accept empty arrays, but set flag*/
        if (0 == count($args)) {
            $this->_noArgs = true;
        } else {
            $this->_noArgs = false;
        }

        $this->_args = $args;

        return true;
    }

    /**
     * Sets the system's path to the traceroute binary
     *
     * @param string $sysname Systemname which identifies the OS
     *
     * @return string Traceroute-name (path to traceroute)
     * @access private
     */
    function _setTraceroutePath($sysname)
    {
        $status    = '';
        $output    = array();
        $traceroute_path = '';

        if ("windows" == $sysname) {
            return "tracert";
        } else {
            $traceroute_path = exec("which traceroute", $output, $status);
            if ($status != 0) {
                foreach (array('/usr/sbin', '/sbin', '/usr/bin', '/bin', '/usr/local/bin') as $test) {
                    if ($status != 0) {
                        $traceroute_path = $test.'/traceroute';
                        if (file_exists($traceroute_path)) {
                            $status = 0;
                        }
                    }
                }
            }
            if ($status != 0) {
                return NET_TRACEROUTE_CANT_LOCATE_TRACEROUTE_BINARY;
            } else {
                return $traceroute_path;
            }
        }

    }

    /**
     * Creates the argument list according to platform differences
     *
     * @return string          Argument line
     * @access private
     */
    function _createArgList()
    {
        $retval     = array();

        $numeric    = "";
        $ttl        = "";
        $deadline   = "";

        foreach ($this->_args AS $option => $value) {
            if (!empty($option) && !is_null($this->_argRelation[$this->_sysname][$option])) {
                if(empty($value)) {
                    ${$option} = $this->_argRelation[$this->_sysname][$option];
                } else {
                    ${$option} = $this->_argRelation[$this->_sysname][$option]." ".escapeshellarg($value)." ";
                }
            }
        }

        switch($this->_sysname) {
        case "linux":
            $retval[0] = $numeric.$ttl.$deadline;
            $retval[1] = "2>&1";
            break;

        case "windows":
            $retval[0] = $numeric.$ttl.$deadline;
            $retval[1] = "";
            break;

        default:
            $retval[0] = "";
            $retval[1] = "";
            break;
        }

        return($retval);
    }

    /**
     * Execute traceroute
     *
     * @param string $host hostname or IP of destination
     *
     * @return object Net_Traceroute_Result
     * @access public
     */
    function traceroute($host)
    {

        $argList = $this->_createArgList();
        $cmd = $this->_traceroute_path." ".$argList[0]." ".$host." ".$argList[1];
        exec(escapeshellcmd($cmd), $this->_result);

        if (!is_array($this->_result)) {
            return PEAR::throwError(NET_TRACEROUTE_FAILED_MSG, NET_TRACEROUTE_FAILED);
        }

        if (count($this->_result) == 0) {
            return PEAR::throwError(NET_TRACEROUTE_HOST_NOT_FOUND_MSG, NET_TRACEROUTE_HOST_NOT_FOUND);
        } else {
            return Net_Traceroute_Result::factory($this->_result, $this->_sysname);
        }
    }

    /**
     * Output errors with PHP trigger_error(). You can silence the errors
     * with prefixing a "@" sign to the function call: @Net_Traceroute::traceroute(..);
     *
     * @param mixed $error PEAR error or a string with the error message
     *
     * @return bool false
     * @access private
     * @author Kai Schr�der <k.schroeder@php.net>
     */
    function raiseError($error)
    {
        if (PEAR::isError($error)) {
            $error = $error->getMessage();
        }
        trigger_error($error, E_USER_WARNING);
        return false;
    }

    /**
     * Creates the argument list according to platform differences
     *
     * @return string          Argument line
     * @access private
     */
    function _initArgRelation()
    {
        $this->_argRelation = array(
                                    "linux" => array (
                                                        "numeric"   => "-n",
                                                        "ttl"       => "-m",
                                                        "deadline"  => "-w"
                                                        ),
                                    "windows" => array (
                                                        "numeric"   => "-d",
                                                        "ttl"       => "-h",
                                                        "deadline"  => "-w"
                                                        )
                               );
    }
}


/**
 * Container class for Net_Traceroute results
 *
 * @category  Net
 * @package   Net_Traceroute
 * @author    Stefan Neufeind <pear.neufeind@speedpartner.de>
 * @copyright 2003-2009 The PHP Group
 * @license   http://www.php.net/license/2_02.txt  PHP License 2.02
 * @version   Release: @package_version@
 * @link      http://pear.php.net/package/Net_Traceroute
 */
class Net_Traceroute_Result
{
    /**
     * Hops and associated time in ms
     *
     * @var    array
     * @access private
     */
    var $_hops = array();

    /**
     * The target's IP Address
     *
     * @var    string
     * @access private
     */
    var $_target_ip;

    /**
     * The ICMP request's TTL
     *
     * @var    int
     * @access private
     */
    var $_ttl;

    /**
     * The raw Net_Traceroute::result
     *
     * @var    array
     * @access private
     */
    var $_raw_data = array();

    /**
     * The Net_Traceroute::_sysname
     *
     * @var    string
     * @access private
     */
    var $_sysname;


    /**
     * Constructor for the Class
     *
     * @param string $result  Result as returned from traceroute
     * @param string $sysname Systemname which identifies the OS
     *
     * @access private
     */
    function Net_Traceroute_Result($result, $sysname)
    {
        $this->_raw_data = $result;
        $this->_sysname  = $sysname;

        $this->_parseResult();
    }

    /**
     * Factory for Net_Traceroute_Result
     *
     * @param array  $result  Net_Traceroute result
     * @param string $sysname OS_Guess::sysname
     *
     * @return object PEAR_Error or Net_Traceroute_Result
     * @access public
     */
    function factory($result, $sysname)
    {
        if (!Net_Traceroute_Result::_prepareParseResult($sysname)) {
            return PEAR::throwError(NET_TRACEROUTE_RESULT_UNSUPPORTED_BACKEND_MSG, NET_TRACEROUTE_RESULT_UNSUPPORTED_BACKEND);
        } else {
            return new Net_Traceroute_Result($result, $sysname);
        }
    }

    /**
     * Preparation method for _parseResult
     *
     * @param string $sysname OS_Guess::sysname
     *
     * @return bool
     * @access private
     */
    function _prepareParseResult($sysname)
    {
        $methods = array_values(array_map('strtolower', get_class_methods('Net_Traceroute_Result')));
        return in_array(strtolower('_parseResult'.$sysname), $methods);
    }

    /**
     * Delegates the parsing routine according to $this->_sysname
     *
     * @return void
     * @see    _parseResultlinux()
     * @see    _parseResultwindows()
     * @access private
     */
    function _parseResult()
    {
        $this->{'_parseResult' . $this->_sysname}();
    }

    /**
     * Parses the output of Linux' traceroute command
     *
     * @return void
     * @see    _parseResult()
     * @access private
     */
    function _parseResultlinux()
    {
        $raw_data_len = count($this->_raw_data);
        $dataRow = 0;

        while (empty($this->_raw_data[$dataRow]) && ($dataRow<$raw_data_len)) {
            $dataRow++;
        }

        $tempparts        = explode(' ', $this->_raw_data[$dataRow]);
        $this->_target_ip = trim($tempparts[3], ' (),');
        $this->_ttl       = (int) $tempparts[4];
        $dataRow++;

        while (empty($this->_raw_data[$dataRow]) && ($dataRow<$raw_data_len)) {
            $dataRow++;
        }

        $hops = array();
        while (($dataRow < $raw_data_len) && !empty($this->_raw_data[$dataRow])) {
            $hop = array();
            $parts = explode('  ', substr($this->_raw_data[$dataRow], 4));

            /* if we can find a next hop it's name/ip will be here */
            if (count($parts) > 0) {
                /* get machine/ip */
                $machineparts = explode(' ', $parts[0]);
                if (count($machineparts) > 1) {
                    $hop['machine'] = $machineparts[0];
                    $hop['ip']      = trim($machineparts[1], ' ()');
                } else {
                    $hop['ip'] = $machineparts[0];
                }
                array_shift($parts);
            }

            $responsetimes = array();
            for ($timeidx = 0; $timeidx < count($parts); $timeidx++) {
                $temppart=explode(' ', $parts[$timeidx]);
                if ($temppart[0] == "*") {
                    $responsetimes[] = -1; // unreachable
                } else {
                    $responsetimes[] = (float) $temppart[0];
                }
            }
            $hop['responsetimes'] = $responsetimes;
            $hops[] = $hop;
            $dataRow++;
        }
        $this->_hops = $hops;
    }

    /**
     * Parses the output of Windows' traceroute command
     *
     * @return void
     * @see    _parseResult()
     * @access private
     */
    function _parseResultwindows()
    {
        $raw_data_len = count($this->_raw_data);
        $dataRow = 0;

        while (empty($this->_raw_data[$dataRow]) && ($dataRow<$raw_data_len)) {
            $dataRow++;
        }

        $tempparts = explode(' ', $this->_raw_data[$dataRow]);
        $searchIdx = 0;
        while (($searchIdx < count($tempparts)) && (substr($tempparts[$searchIdx], 0, 1) != '[')) {
            $searchIdx++;
        }
        $this->_target_ip = trim($tempparts[$searchIdx], ' [],');
        while (($searchIdx < count($tempparts)) && ((int) $tempparts[$searchIdx] <= 0)) {
            $searchIdx++;
        }
        if ((int) $tempparts[$searchIdx] > 0) {
            $this->_ttl = (int) $tempparts[$searchIdx]; // TTL might be written in next line; e.g. on Windows 98
        } elseif (!empty($this->_raw_data[$dataRow+1])) {
            $dataRow++;
            $tempparts  = explode(' ', $this->_raw_data[$dataRow]);
            $searchIdx = 0;
            while (($searchIdx < count($tempparts)) && ((int) $tempparts[$searchIdx] <= 0)) {
                $searchIdx++;
            }
            if ((int) $tempparts[$searchIdx] > 0) {
                $this->_ttl       = (int) $tempparts[$searchIdx]; // TTL might be written in next line; e.g. on Windows 98
            }
        }

        while (!empty($this->_raw_data[$dataRow]) && ($dataRow<$raw_data_len)) {
            $dataRow++;
        }
        while (empty($this->_raw_data[$dataRow]) && ($dataRow<$raw_data_len)) {
            $dataRow++;
        }

        $hops=array();
        /* loop from second elment to the fifths last */
        while (($dataRow < $raw_data_len) && !empty($this->_raw_data[$dataRow])) {
            $hop=array();

            $responsetimes = array();
            for ($timeidx = 0; $timeidx < 3; $timeidx++) {
                $temppart = trim(str_replace(' ms', '', substr($this->_raw_data[$dataRow], 3+($timeidx*9), 9)));
                if ($temppart == '*') {
                    $responsetimes[] = -1; // unreachable
                } else {
                    $responsetimes[] = (float) str_replace('<', '', $temppart);
                }
            }
            $hop['responsetimes'] = $responsetimes;

            $machineparts = explode(' ', rtrim(substr($this->_raw_data[$dataRow], 32)));
            // if we can find a next hop it's name/ip will be here
            if (count($machineparts) == 1) {
                $hop['ip'] = trim($machineparts[0], ' ()[]');
            } elseif (count($machineparts) == 2) {
                $hop['machine'] = $machineparts[0];
                $hop['ip']      = trim($machineparts[1], ' ()[]');
            }
            // otherwise we've got an errormessage or something here ... like "time limit exceeded"

            $hops[] = $hop;
            $dataRow++;
        }
        $this->_hops = $hops;
    }

    /**
     * Returns a Traceroute_Result property
     *
     * @param string $name property name
     *
     * @return mixed property value
     * @access public
     */
    function getValue($name)
    {
        return isset($this->$name) ? $this->$name : '';
    }

    /**
     * Returns the target IP from parsed result
     *
     * @return string          IP address
     * @see    _target_ip
     * @access public
     */
    function getTargetIp()
    {
        return $this->_target_ip;
    }

    /**
     * Returns hops from parsed result
     *
     * @return array           Hops
     * @see    _hops
     * @access public
     */
    function getHops()
    {
        return $this->_hops;
    }

    /**
     * Returns TTL from parsed result
     *
     * @return int             TTL
     * @see    _ttl
     * @access public
     */
    function getTTL()
    {
        return $this->_ttl;
    }

    /**
     * Returns raw data that was returned by traceroute
     *
     * @return array           raw data
     * @see    _raw_data
     * @access public
     */
    function getRawData()
    {
        return $this->_raw_data;
    }

    /**
     * Returns sysname that was "guessed" (OS on which class is running)
     *
     * @return string          OS_Guess::sysname
     * @see    _sysname
     * @access public
     */
    function getSystemName()
    {
        return $this->_sysname;
    }
}
?>
