From f6a470e8f8c4b09575dabc45a92876ccb6bd477b Mon Sep 17 00:00:00 2001 From: Marc Hagen Date: Wed, 23 Nov 2022 11:48:40 +0100 Subject: [PATCH] Add live MTR to LookingGlass --- LookingGlass.php | 223 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 206 insertions(+), 17 deletions(-) diff --git a/LookingGlass.php b/LookingGlass.php index 49962e5..131be53 100644 --- a/LookingGlass.php +++ b/LookingGlass.php @@ -180,7 +180,7 @@ class LookingGlass */ public static function mtr(string $host): bool { - return self::procExecute('mtr -4 --report --report-wide', $host); + return self::procExecute('mtr --raw -n -4 -c ' . self::MTR_COUNT, $host); } /** @@ -191,7 +191,7 @@ class LookingGlass */ public static function mtr6(string $host): bool { - return self::procExecute('mtr -6 --report --report-wide', $host); + return self::procExecute('mtr --raw -n -6 -c ' . self::MTR_COUNT, $host); } /** @@ -232,11 +232,11 @@ class LookingGlass private static function procExecute(string $cmd, string $host, int $failCount = 2): bool { // define output pipes - $spec = array( - 0 => array("pipe", "r"), - 1 => array("pipe", "w"), - 2 => array("pipe", "w") - ); + $spec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'] + ]; // sanitize + remove single quotes $host = str_replace('\'', '', filter_var($host, FILTER_SANITIZE_URL)); @@ -251,16 +251,17 @@ class LookingGlass // check for mtr/traceroute if (strpos($cmd, 'mtr') !== false) { $type = 'mtr'; + $parser = new Parser(); } elseif (strpos($cmd, 'traceroute') !== false) { $type = 'traceroute'; } else { $type = ''; } - $fail = 0; - $match = 0; + $fail = 0; + $match = 0; $traceCount = 0; - $lastFail = 'start'; + $lastFail = 'start'; // iterate stdout while (($str = fgets($pipes[1], 4096)) != null) { // check for output buffer @@ -273,12 +274,14 @@ class LookingGlass // correct output for mtr if ($type === 'mtr') { - if ($match < 10 && preg_match('/^[0-9]\. /', $str, $string)) { - $str = preg_replace('/^[0-9]\. /', '  ' . $string[0], $str); - $match++; - } else { - $str = preg_replace('/^[0-9]{2}\. /', ' ' . substr($str, 0, 4), $str); - } + // correct output for mtr + $parser->update($str); + echo '---' . PHP_EOL . $parser->__toString() . PHP_EOL . str_pad('', 4096) . PHP_EOL; + + // flush output buffering + @ob_flush(); + flush(); + continue; } // correct output for traceroute elseif ($type === 'traceroute') { @@ -319,7 +322,7 @@ class LookingGlass } $status = proc_get_status($process); - if ($status['running'] == true) { + if ($status['running']) { // close pipes that are still open foreach ($pipes as $pipe) { fclose($pipe); @@ -341,3 +344,189 @@ class LookingGlass return true; } } + +class Hop +{ + /** @var int */ + public $idx; + /** @var string */ + public $asn = ''; + /** @var float */ + public $avg = 0.0; + /** @var int */ + public $loss = 0; + /** @var float */ + public $stdev = 0.0; + /** @var int */ + public $sent = 0; + /** @var int */ + public $recieved = 0; + /** @var float */ + public $last = 0.0; + /** @var float */ + public $best = 0.0; + /** @var float */ + public $worst = 0.0; + + /** @var string[] */ + public $ips = []; + /** @var string[] */ + public $hosts = []; + /** @var float[] */ + public $timings = []; + +} + +class RawHop +{ + /** @var string */ + public $dataType; + /** @var int */ + public $idx; + /** @var string */ + public $value; +} + +class Parser +{ + /** @var Hop[] */ + protected $hopsCollection = []; + /** @var int */ + private $hopCount = 0; + /** @var int */ + private $outputWidth = 38; + + public function __construct() + { + putenv('RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1'); + } + + public function __toString(): string + { + $str = ''; + foreach ($this->hopsCollection as $index => $hop) { + $host = $hop->hosts[0] ?? $hop->ips[0] ?? '???'; + + if (strlen($host) > $this->outputWidth) { + $this->outputWidth = strlen($host); + } + + $hop->recieved = count($hop->timings); + if (count($hop->timings)) { + $hop->last = $hop->timings[count($hop->timings) - 1]; + $hop->best = $hop->timings[0]; + $hop->worst = $hop->timings[0]; + $hop->avg = array_sum($hop->timings) / count($hop->timings); + } + + if (count($hop->timings) > 1) { + $hop->stdev = $this->stDev($hop->timings); + } + + foreach ($hop->timings as $time) { + + if ($hop->best > $time) { + $hop->best = $time; + } + + if ($hop->worst < $time) { + $hop->worst = $time; + } + } + + $hop->loss = $hop->sent ? (100 * ($hop->sent - $hop->recieved)) / $hop->sent : 100; + + $str = sprintf( + "%s%2d.|-- %s%3d.0%% %3d %5.1f %5.1f %5.1f %5.1f %5.1f\n", + $str, + $index, + str_pad($host, $this->outputWidth + 3, ' ', STR_PAD_RIGHT), + $hop->loss, + $hop->sent, + $hop->last, + $hop->avg, + $hop->best, + $hop->worst, + $hop->stdev + ); + } + + return sprintf(" Host%sLoss%% Snt Last Avg Best Wrst StDev\n%s", str_pad('', $this->outputWidth + 7, ' ', STR_PAD_RIGHT), $str); + } + + private function stDev(array $array): float + { + $sdSquare = function ($x, $mean) { + return pow($x - $mean, 2); + }; + + // square root of sum of squares devided by N-1 + return sqrt(array_sum(array_map($sdSquare, $array, array_fill(0, count($array), (array_sum($array) / count($array))))) / (count($array) - 1)); + } + + public function update($rawMtrInput) + { + //Store each line of output in rawhop structure + $things = explode(' ', $rawMtrInput); + + if (count($things) !== 3 && (count($things) !== 4 && $things[0] === 'p')) { + return; + } + + $rawHop = new RawHop(); + $rawHop->dataType = $things[0]; + $rawHop->idx = (int)$things[1]; + $rawHop->value = $things[2]; + + if ($this->hopCount < $rawHop->idx + 1) { + $this->hopCount = $rawHop->idx + 1; + } + + if (!isset($this->hopsCollection[$rawHop->idx])) { + $this->hopsCollection[$rawHop->idx] = new Hop(); + } + + $hop = $this->hopsCollection[$rawHop->idx]; + $hop->idx = $rawHop->idx; + switch ($rawHop->dataType) { + case 'h': + $hop->ips[] = $rawHop->value; + $hop->hosts[] = gethostbyaddr($rawHop->value) ? : null; + break; + case 'd': + //Not entirely sure if multiple IPs. Better use -n in mtr and resolve later in summarize. + //out.Hops[data.idx].Host = append(out.Hops[data.idx].Host, data.value) + break; + case 'p': + $hop->sent++; + $hop->timings[] = (float)$rawHop->value / 1000; + break; + } + + $this->hopsCollection[$rawHop->idx] = $hop; + + $this->filterLastDupeHop(); + } + + // Function to calculate standard deviation (uses sd_square) + + private function filterLastDupeHop() + { + // filter dupe last hop + $finalIdx = 0; + $previousIp = ''; + + foreach ($this->hopsCollection as $key => $hop) { + if (count($hop->ips) && $hop->ips[0] !== $previousIp) { + $previousIp = $hop->ips[0]; + $finalIdx = $key + 1; + } + } + + unset($this->hopsCollection[$finalIdx]); + + usort($this->hopsCollection, function ($a, $b) { + return $a->idx - $b->idx; + }); + } +}