Subversion Repositories ALCASAR

Rev

Blame | Last modification | View Log

<?php

namespace mbolli\nfsen_ng\processor;

use mbolli\nfsen_ng\common\Config;
use mbolli\nfsen_ng\common\Debug;

class Nfdump implements Processor {
    private array $cfg = [
        'env' => [],
        'option' => [],
        'format' => null,
        'filter' => [],
    ];
    private array $clean;
    private readonly Debug $d;
    public static ?self $_instance = null;

    public function __construct() {
        $this->d = Debug::getInstance();
        $this->clean = $this->cfg;
        $this->reset();
    }

    public static function getInstance(): self {
        if (!(self::$_instance instanceof self)) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }

    /**
     * Sets an option's value.
     */
    public function setOption(string $option, $value): void {
        switch ($option) {
            case '-M': // set sources
                // only sources specified in settings allowed
                $queried_sources = explode(':', (string) $value);
                foreach ($queried_sources as $s) {
                    if (!\in_array($s, Config::$cfg['general']['sources'], true)) {
                        continue;
                    }
                    $this->cfg['env']['sources'][] = $s;
                }

                // cancel if no sources remain
                if (empty($this->cfg['env']['sources'])) {
                    break;
                }

                // set sources path
                $this->cfg['option'][$option] = implode(\DIRECTORY_SEPARATOR, [
                    $this->cfg['env']['profiles-data'],
                    $this->cfg['env']['profile'],
                    implode(':', $this->cfg['env']['sources']),
                ]);

                break;
            case '-R': // set path
                $this->cfg['option'][$option] = $this->convert_date_to_path($value[0], $value[1]);
                break;
            case '-o': // set output format
                $this->cfg['format'] = $value;
                break;
            default:
                $this->cfg['option'][$option] = $value;
                $this->cfg['option']['-o'] = 'csv'; // always get parsable data todo user-selectable? calculations bps/bpp/pps not in csv
                break;
        }
    }

    /**
     * Sets a filter's value.
     */
    public function setFilter(string $filter): void {
        $this->cfg['filter'] = $filter;
    }

    /**
     * Executes the nfdump command, tries to throw an exception based on the return code.
     *
     * @throws \Exception
     */
    public function execute(): array {
        $output = [];
        $processes = [];
        $return = '';
        $timer = microtime(true);
        $filter = (empty($this->cfg['filter'])) ? '' : ' ' . escapeshellarg((string) $this->cfg['filter']);
        $command = $this->cfg['env']['bin'] . ' ' . $this->flatten($this->cfg['option']) . $filter . ' 2>&1';
        $this->d->log('Trying to execute ' . $command, \LOG_DEBUG);

        // check for already running nfdump processes
        exec('ps -eo user,pid,args | grep -v grep | grep `whoami` | grep "' . $this->cfg['env']['bin'] . '"', $processes);
        if (\count($processes) / 2 > (int) Config::$cfg['nfdump']['max-processes']) {
            throw new \Exception('There already are ' . \count($processes) / 2 . ' processes of NfDump running!');
        }

        // execute nfdump
        exec($command, $output, $return);

        // prevent logging the command usage description
        if (isset($output[0]) && preg_match('/^usage/i', $output[0])) {
            $output = [];
        }

        switch ($return) {
            case 127:
                throw new \Exception('NfDump: Failed to start process. Is nfdump installed? <br><b>Output:</b> ' . implode(' ', $output));
            case 255:
                throw new \Exception('NfDump: Initialization failed. ' . $command . '<br><b>Output:</b> ' . implode(' ', $output));
            case 254:
                throw new \Exception('NfDump: Error in filter syntax. <br><b>Output:</b> ' . implode(' ', $output));
            case 250:
                throw new \Exception('NfDump: Internal error. <br><b>Output:</b> ' . implode(' ', $output));
        }

        // add command to output
        array_unshift($output, $command);

        // if last element contains a colon, it's not a csv
        if (str_contains($output[\count($output) - 1], ':')) {
            return $output; // return output if it is a flows/packets/bytes dump
        }

        // remove the 3 summary lines at the end of the csv output
        $output = \array_slice($output, 0, -3);

        // slice csv (only return the fields actually wanted)
        $field_ids_active = [];
        $parsed_header = false;
        $format = false;
        if (isset($this->cfg['format'])) {
            $format = $this->get_output_format($this->cfg['format']);
        }

        foreach ($output as $i => &$line) {
            if ($i === 0) {
                continue;
            } // skip nfdump command
            $line = str_getcsv($line, ',');
            $temp_line = [];

            if (\count($line) === 1 || preg_match('/limit/', $line[0]) || preg_match('/error/', $line[0])) { // probably an error message or warning. add to command
                $output[0] .= ' <br><b>' . $line[0] . '</b>';
                unset($output[$i]);
                continue;
            }
            if (!\is_array($format)) {
                $format = $line;
            } // set first valid line as header if not already defined

            foreach ($line as $field_id => $field) {
                // heading has the field identifiers. fill $fields_active with all active fields
                if ($parsed_header === false) {
                    if (\in_array($field, $format, true)) {
                        $field_ids_active[array_search($field, $format, true)] = $field_id;
                    }
                }

                // remove field if not in $fields_active
                if (\in_array($field_id, $field_ids_active, true)) {
                    $temp_line[array_search($field_id, $field_ids_active, true)] = $field;
                }
            }

            $parsed_header = true;
            ksort($temp_line);
            $line = array_values($temp_line);
        }

        // add execution time to output
        $output[0] .= '<br><b>Execution time:</b> ' . round(microtime(true) - $timer, 3) . ' seconds';

        return array_values($output);
    }

    /**
     * Concatenates key and value of supplied array.
     */
    private function flatten(array $array): string {
        $output = '';

        foreach ($array as $key => $value) {
            if ($value === null) {
                $output .= $key . ' ';
            } else {
                $output .= \is_int($key) ?: $key . ' ' . escapeshellarg((string) $value) . ' ';
            }
        }

        return $output;
    }

    /**
     * Reset config.
     */
    public function reset(): void {
        $this->clean['env'] = [
            'bin' => Config::$cfg['nfdump']['binary'],
            'profiles-data' => Config::$cfg['nfdump']['profiles-data'],
            'profile' => Config::$cfg['nfdump']['profile'],
            'sources' => [],
        ];
        $this->cfg = $this->clean;
    }

    /**
     * Converts a time range to a nfcapd file range
     * Ensures that files actually exist.
     *
     * @throws \Exception
     */
    public function convert_date_to_path(int $datestart, int $dateend): string {
        $start = new \DateTime();
        $end = new \DateTime();
        $start->setTimestamp((int) $datestart - ($datestart % 300));
        $end->setTimestamp((int) $dateend - ($dateend % 300));
        $filestart = $fileend = '-';
        $filestartexists = false;
        $fileendexists = false;
        $sourcepath = $this->cfg['env']['profiles-data'] . \DIRECTORY_SEPARATOR . $this->cfg['env']['profile'] . \DIRECTORY_SEPARATOR;

        // if start file does not exist, increment by 5 minutes and try again
        while ($filestartexists === false) {
            if ($start >= $end) {
                break;
            }

            foreach ($this->cfg['env']['sources'] as $source) {
                if (file_exists($sourcepath . $source . \DIRECTORY_SEPARATOR . $filestart)) {
                    $filestartexists = true;
                }
            }

            $pathstart = $start->format('Y/m/d') . \DIRECTORY_SEPARATOR;
            $filestart = $pathstart . 'nfcapd.' . $start->format('YmdHi');
            $start->add(new \DateInterval('PT5M'));
        }

        // if end file does not exist, subtract by 5 minutes and try again
        while ($fileendexists === false) {
            if ($end === $start) { // strict comparison won't work
                $fileend = $filestart;
                break;
            }

            foreach ($this->cfg['env']['sources'] as $source) {
                if (file_exists($sourcepath . $source . \DIRECTORY_SEPARATOR . $fileend)) {
                    $fileendexists = true;
                }
            }

            $pathend = $end->format('Y/m/d') . \DIRECTORY_SEPARATOR;
            $fileend = $pathend . 'nfcapd.' . $end->format('YmdHi');
            $end->sub(new \DateInterval('PT5M'));
        }

        return $filestart . \PATH_SEPARATOR . $fileend;
    }

    public function get_output_format($format): array {
        // todo calculations like bps/pps? flows? concatenate sa/sp to sap?
        return match ($format) {
            'line' => ['ts', 'td', 'pr', 'sa', 'sp', 'da', 'dp', 'ipkt', 'ibyt', 'fl'],
            'long' => ['ts', 'td', 'pr', 'sa', 'sp', 'da', 'dp', 'flg', 'stos', 'dtos', 'ipkt', 'ibyt', 'fl'],
            'extended' => ['ts', 'td', 'pr', 'sa', 'sp', 'da', 'dp', 'ipkt', 'ibyt', 'ibps', 'ipps', 'ibpp'],
            'full' => ['ts', 'te', 'td', 'sa', 'da', 'sp', 'dp', 'pr', 'flg', 'fwd', 'stos', 'ipkt', 'ibyt', 'opkt', 'obyt', 'in', 'out', 'sas', 'das', 'smk', 'dmk', 'dtos', 'dir', 'nh', 'nhb', 'svln', 'dvln', 'ismc', 'odmc', 'idmc', 'osmc', 'mpls1', 'mpls2', 'mpls3', 'mpls4', 'mpls5', 'mpls6', 'mpls7', 'mpls8', 'mpls9', 'mpls10', 'cl', 'sl', 'al', 'ra', 'eng', 'exid', 'tr'],
            default => explode(' ', str_replace(['fmt:', '%'], '', (string) $format)),
        };
    }
}