Subversion Repositories ALCASAR

Rev

Blame | Last modification | View Log

<?php

namespace mbolli\nfsen_ng\api;

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

class Api {
    private readonly string $method;
    private array $request;

    public function __construct() {
        header('Content-Type: application/json');
        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: deny');

        // try to read config
        try {
            Config::initialize(true);
        } catch (\Exception $e) {
            $this->error(503, $e->getMessage());
        }

        // get the HTTP method, path and body of the request
        $this->method = $_SERVER['REQUEST_METHOD'];
        $this->request = explode('/', trim((string) $_GET['request'], '/'));

        // only allow GET requests
        // if at some time POST requests are enabled, check the request's content type (or return 406)
        if ($this->method !== 'GET') {
            $this->error(501);
        }

        // call correct method
        if (!method_exists($this, $this->request[0])) {
            $this->error(404);
        }

        // remove method name from $_REQUEST
        $_REQUEST = array_filter($_REQUEST, fn ($x) => $x !== $this->request[0]);

        $method = new \ReflectionMethod($this, $this->request[0]);

        // check number of parameters
        if ($method->getNumberOfRequiredParameters() > \count($_REQUEST)) {
            $this->error(400, 'Not enough parameters');
        }

        $args = [];
        // iterate over each parameter
        foreach ($method->getParameters() as $arg) {
            if (!isset($_REQUEST[$arg->name])) {
                if ($arg->isOptional()) {
                    continue;
                }
                $this->error(400, 'Expected parameter ' . $arg->name);
            }

            /** @var ?\ReflectionNamedType $namedType */
            $namedType = $arg->getType();
            if ($namedType === null) {
                continue;
            }

            // make sure the data types are correct
            switch ($namedType->getName()) {
                case 'int':
                    if (!is_numeric($_REQUEST[$arg->name])) {
                        $this->error(400, 'Expected type int for ' . $arg->name);
                    }
                    $args[$arg->name] = (int) $_REQUEST[$arg->name];
                    break;
                case 'array':
                    if (!\is_array($_REQUEST[$arg->name])) {
                        $this->error(400, 'Expected type array for ' . $arg->name);
                    }
                    $args[$arg->name] = $_REQUEST[$arg->name];
                    break;
                case 'string':
                    if (!\is_string($_REQUEST[$arg->name])) {
                        $this->error(400, 'Expected type string for ' . $arg->name);
                    }
                    $args[$arg->name] = $_REQUEST[$arg->name];
                    break;
                default:
                    $args[$arg->name] = $_REQUEST[$arg->name];
            }
        }

        // get output
        $output = $this->{$this->request[0]}(...array_values($args));

        // return output
        if (\array_key_exists('csv', $_REQUEST)) {
            // output CSV
            header('Content-Type: text/csv; charset=utf-8');
            header('Content-Disposition: attachment; filename=export.csv');
            $return = fopen('php://output', 'w');
            foreach ($output as $i => $line) {
                if ($i === 0) {
                    continue;
                } // skip first line
                fputcsv($return, $line);
            }

            fclose($return);
        } else {
            // output JSON
            echo json_encode($output, \JSON_THROW_ON_ERROR);
        }
    }

    /**
     * Helper function, returns the http status and exits the application.
     *
     * @throws \JsonException
     */
    public function error(int $code, string $msg = ''): never {
        http_response_code($code);
        $debug = Debug::getInstance();

        $response = ['code' => $code, 'error' => ''];
        switch ($code) {
            case 400:
                $response['error'] = '400 - Bad Request. ' . (empty($msg) ? 'Probably wrong or not enough arguments.' : $msg);
                $debug->log($response['error'], \LOG_INFO);
                break;
            case 401:
                $response['error'] = '401 - Unauthorized. ' . $msg;
                $debug->log($response['error'], \LOG_WARNING);
                break;
            case 403:
                $response['error'] = '403 - Forbidden. ' . $msg;
                $debug->log($response['error'], \LOG_WARNING);
                break;
            case 404:
                $response['error'] = '404 - Not found. ' . $msg;
                $debug->log($response['error'], \LOG_WARNING);
                break;
            case 501:
                $response['error'] = '501 - Method not implemented. ' . $msg;
                $debug->log($response['error'], \LOG_WARNING);
                break;
            case 503:
                $response['error'] = '503 - Service unavailable. ' . $msg;
                $debug->log($response['error'], \LOG_ERR);
                break;
        }
        echo json_encode($response, \JSON_THROW_ON_ERROR);
        exit;
    }

    /**
     * Execute the processor to get statistics.
     *
     * @return array
     */
    public function stats(
        int $datestart,
        int $dateend,
        array $sources,
        string $filter,
        int $top,
        string $for,
        string $limit,
        array $output = [],
    ) {
        $sources = implode(':', $sources);

        $processor = Config::$processorClass;
        $processor->setOption('-M', $sources); // multiple sources
        $processor->setOption('-R', [$datestart, $dateend]); // date range
        $processor->setOption('-n', $top);
        if (\array_key_exists('format', $output)) {
            $processor->setOption('-o', $output['format']);

            if ($output['format'] === 'custom' && \array_key_exists('custom', $output) && !empty($output['custom'])) {
                $processor->setOption('-o', 'fmt:' . $output['custom']);
            }
        }

        $processor->setOption('-s', $for);
        if (!empty($limit)) {
            $processor->setOption('-l', $limit);
        } // todo -L for traffic, -l for packets

        $processor->setFilter($filter);

        try {
            return $processor->execute();
        } catch (\Exception $e) {
            $this->error(503, $e->getMessage());
        }
    }

    /**
     * Execute the processor to get flows.
     *
     * @return array
     */
    public function flows(
        int $datestart,
        int $dateend,
        array $sources,
        string $filter,
        int $limit,
        string $aggregate,
        string $sort,
        array $output,
    ) {
        $aggregate_command = '';
        // nfdump -M /srv/nfsen/profiles-data/live/tiber:n048:gate:swibi:n055:swi6  -T  -r 2017/04/10/nfcapd.201704101150 -c 20
        $sources = implode(':', $sources);
        if (!empty($aggregate)) {
            $aggregate_command = ($aggregate === 'bidirectional') ? '-B' : '-A' . $aggregate;
        } // no space inbetween

        $processor = new Config::$processorClass();
        $processor->setOption('-M', $sources); // multiple sources
        $processor->setOption('-R', [$datestart, $dateend]); // date range
        $processor->setOption('-c', $limit); // limit
        $processor->setOption('-o', $output['format']);
        if ($output['format'] === 'custom' && \array_key_exists('custom', $output) && !empty($output['custom'])) {
            $processor->setOption('-o', 'fmt:' . $output['custom']);
        }

        if (!empty($sort)) {
            $processor->setOption('-O', 'tstart');
        }
        if (!empty($aggregate_command)) {
            $processor->setOption('-a', $aggregate_command);
        }
        $processor->setFilter($filter);

        try {
            $return = $processor->execute();
        } catch (\Exception $e) {
            $this->error(503, $e->getMessage());
        }

        return $return;
    }

    /**
     * Get data to build a graph.
     *
     * @return array|string
     */
    public function graph(
        int $datestart,
        int $dateend,
        array $sources,
        array $protocols,
        array $ports,
        string $type,
        string $display,
    ) {
        $graph = Config::$db->get_graph_data($datestart, $dateend, $sources, $protocols, $ports, $type, $display);
        if (!\is_array($graph)) {
            $this->error(400, $graph);
        }

        return $graph;
    }

    public function graph_stats(): void {}

    /**
     * Get config info.
     *
     * @return array
     */
    public function config() {
        $sources = Config::$cfg['general']['sources'];
        $ports = Config::$cfg['general']['ports'];
        $frontend = Config::$cfg['frontend'];

        $stored_output_formats = []; // todo implement
        $stored_filters =  Config::$cfg['general']['filters'];

        $folder = \dirname(__FILE__, 2);
        $pidfile = $folder . '/nfsen-ng.pid';
        $daemon_running = file_exists($pidfile);

        // get date of first data point
        $firstDataPoint = \PHP_INT_MAX;
        foreach ($sources as $source) {
            $firstDataPoint = min($firstDataPoint, Config::$db->date_boundaries($source)[0]);
        }
        $frontend['data_start'] = $firstDataPoint;

        return [
            'sources' => $sources,
            'ports' => $ports,
            'stored_output_formats' => $stored_output_formats,
            'stored_filters' => $stored_filters,
            'daemon_running' => $daemon_running,
            'frontend' => $frontend,
            'version' => Config::VERSION,
            'tz_offset' => (new \DateTimeZone(date_default_timezone_get()))->getOffset(new \DateTime('now', new \DateTimeZone('UTC'))) / 3600,
        ];
    }

    /**
     * executes the host command with a timeout of 5 seconds.
     */
    public function host(string $ip): string {
        try {
            // check ip format
            if (!filter_var($ip, \FILTER_VALIDATE_IP)) {
                $this->error(400, 'Invalid IP address');
            }

            exec('host -W 5 ' . $ip, $output, $return_var);
            if ($return_var !== 0) {
                $this->error(404, 'Host command failed');
            }

            $output = implode(' ', $output);
            if (!preg_match('/domain name pointer (.*)\./', $output, $matches)) {
                $this->error(500, "Could not parse host output: {$output}");
            }

            return $matches[1];
        } catch (\Exception $e) {
            $this->error(500, $e->getMessage());
        }
    }
}