Subversion Repositories ALCASAR

Rev

Details | Last modification | View Log

Rev Author Line No. Line
3241 rexy 1
<?php
2
 
3
namespace mbolli\nfsen_ng\api;
4
 
5
use mbolli\nfsen_ng\common\Config;
6
use mbolli\nfsen_ng\common\Debug;
7
 
8
class Api {
9
    private readonly string $method;
10
    private array $request;
11
 
12
    public function __construct() {
13
        header('Content-Type: application/json');
14
        header('X-Content-Type-Options: nosniff');
15
        header('X-Frame-Options: deny');
16
 
17
        // try to read config
18
        try {
19
            Config::initialize(true);
20
        } catch (\Exception $e) {
21
            $this->error(503, $e->getMessage());
22
        }
23
 
24
        // get the HTTP method, path and body of the request
25
        $this->method = $_SERVER['REQUEST_METHOD'];
26
        $this->request = explode('/', trim((string) $_GET['request'], '/'));
27
 
28
        // only allow GET requests
29
        // if at some time POST requests are enabled, check the request's content type (or return 406)
30
        if ($this->method !== 'GET') {
31
            $this->error(501);
32
        }
33
 
34
        // call correct method
35
        if (!method_exists($this, $this->request[0])) {
36
            $this->error(404);
37
        }
38
 
39
        // remove method name from $_REQUEST
40
        $_REQUEST = array_filter($_REQUEST, fn ($x) => $x !== $this->request[0]);
41
 
42
        $method = new \ReflectionMethod($this, $this->request[0]);
43
 
44
        // check number of parameters
45
        if ($method->getNumberOfRequiredParameters() > \count($_REQUEST)) {
46
            $this->error(400, 'Not enough parameters');
47
        }
48
 
49
        $args = [];
50
        // iterate over each parameter
51
        foreach ($method->getParameters() as $arg) {
52
            if (!isset($_REQUEST[$arg->name])) {
53
                if ($arg->isOptional()) {
54
                    continue;
55
                }
56
                $this->error(400, 'Expected parameter ' . $arg->name);
57
            }
58
 
59
            /** @var ?\ReflectionNamedType $namedType */
60
            $namedType = $arg->getType();
61
            if ($namedType === null) {
62
                continue;
63
            }
64
 
65
            // make sure the data types are correct
66
            switch ($namedType->getName()) {
67
                case 'int':
68
                    if (!is_numeric($_REQUEST[$arg->name])) {
69
                        $this->error(400, 'Expected type int for ' . $arg->name);
70
                    }
71
                    $args[$arg->name] = (int) $_REQUEST[$arg->name];
72
                    break;
73
                case 'array':
74
                    if (!\is_array($_REQUEST[$arg->name])) {
75
                        $this->error(400, 'Expected type array for ' . $arg->name);
76
                    }
77
                    $args[$arg->name] = $_REQUEST[$arg->name];
78
                    break;
79
                case 'string':
80
                    if (!\is_string($_REQUEST[$arg->name])) {
81
                        $this->error(400, 'Expected type string for ' . $arg->name);
82
                    }
83
                    $args[$arg->name] = $_REQUEST[$arg->name];
84
                    break;
85
                default:
86
                    $args[$arg->name] = $_REQUEST[$arg->name];
87
            }
88
        }
89
 
90
        // get output
91
        $output = $this->{$this->request[0]}(...array_values($args));
92
 
93
        // return output
94
        if (\array_key_exists('csv', $_REQUEST)) {
95
            // output CSV
96
            header('Content-Type: text/csv; charset=utf-8');
97
            header('Content-Disposition: attachment; filename=export.csv');
98
            $return = fopen('php://output', 'w');
99
            foreach ($output as $i => $line) {
100
                if ($i === 0) {
101
                    continue;
102
                } // skip first line
103
                fputcsv($return, $line);
104
            }
105
 
106
            fclose($return);
107
        } else {
108
            // output JSON
109
            echo json_encode($output, \JSON_THROW_ON_ERROR);
110
        }
111
    }
112
 
113
    /**
114
     * Helper function, returns the http status and exits the application.
115
     *
116
     * @throws \JsonException
117
     */
118
    public function error(int $code, string $msg = ''): never {
119
        http_response_code($code);
120
        $debug = Debug::getInstance();
121
 
122
        $response = ['code' => $code, 'error' => ''];
123
        switch ($code) {
124
            case 400:
125
                $response['error'] = '400 - Bad Request. ' . (empty($msg) ? 'Probably wrong or not enough arguments.' : $msg);
126
                $debug->log($response['error'], \LOG_INFO);
127
                break;
128
            case 401:
129
                $response['error'] = '401 - Unauthorized. ' . $msg;
130
                $debug->log($response['error'], \LOG_WARNING);
131
                break;
132
            case 403:
133
                $response['error'] = '403 - Forbidden. ' . $msg;
134
                $debug->log($response['error'], \LOG_WARNING);
135
                break;
136
            case 404:
137
                $response['error'] = '404 - Not found. ' . $msg;
138
                $debug->log($response['error'], \LOG_WARNING);
139
                break;
140
            case 501:
141
                $response['error'] = '501 - Method not implemented. ' . $msg;
142
                $debug->log($response['error'], \LOG_WARNING);
143
                break;
144
            case 503:
145
                $response['error'] = '503 - Service unavailable. ' . $msg;
146
                $debug->log($response['error'], \LOG_ERR);
147
                break;
148
        }
149
        echo json_encode($response, \JSON_THROW_ON_ERROR);
150
        exit;
151
    }
152
 
153
    /**
154
     * Execute the processor to get statistics.
155
     *
156
     * @return array
157
     */
158
    public function stats(
159
        int $datestart,
160
        int $dateend,
161
        array $sources,
162
        string $filter,
163
        int $top,
164
        string $for,
165
        string $limit,
166
        array $output = [],
167
    ) {
168
        $sources = implode(':', $sources);
169
 
170
        $processor = Config::$processorClass;
171
        $processor->setOption('-M', $sources); // multiple sources
172
        $processor->setOption('-R', [$datestart, $dateend]); // date range
173
        $processor->setOption('-n', $top);
174
        if (\array_key_exists('format', $output)) {
175
            $processor->setOption('-o', $output['format']);
176
 
177
            if ($output['format'] === 'custom' && \array_key_exists('custom', $output) && !empty($output['custom'])) {
178
                $processor->setOption('-o', 'fmt:' . $output['custom']);
179
            }
180
        }
181
 
182
        $processor->setOption('-s', $for);
183
        if (!empty($limit)) {
184
            $processor->setOption('-l', $limit);
185
        } // todo -L for traffic, -l for packets
186
 
187
        $processor->setFilter($filter);
188
 
189
        try {
190
            return $processor->execute();
191
        } catch (\Exception $e) {
192
            $this->error(503, $e->getMessage());
193
        }
194
    }
195
 
196
    /**
197
     * Execute the processor to get flows.
198
     *
199
     * @return array
200
     */
201
    public function flows(
202
        int $datestart,
203
        int $dateend,
204
        array $sources,
205
        string $filter,
206
        int $limit,
207
        string $aggregate,
208
        string $sort,
209
        array $output,
210
    ) {
211
        $aggregate_command = '';
212
        // nfdump -M /srv/nfsen/profiles-data/live/tiber:n048:gate:swibi:n055:swi6  -T  -r 2017/04/10/nfcapd.201704101150 -c 20
213
        $sources = implode(':', $sources);
214
        if (!empty($aggregate)) {
215
            $aggregate_command = ($aggregate === 'bidirectional') ? '-B' : '-A' . $aggregate;
216
        } // no space inbetween
217
 
218
        $processor = new Config::$processorClass();
219
        $processor->setOption('-M', $sources); // multiple sources
220
        $processor->setOption('-R', [$datestart, $dateend]); // date range
221
        $processor->setOption('-c', $limit); // limit
222
        $processor->setOption('-o', $output['format']);
223
        if ($output['format'] === 'custom' && \array_key_exists('custom', $output) && !empty($output['custom'])) {
224
            $processor->setOption('-o', 'fmt:' . $output['custom']);
225
        }
226
 
227
        if (!empty($sort)) {
228
            $processor->setOption('-O', 'tstart');
229
        }
230
        if (!empty($aggregate_command)) {
231
            $processor->setOption('-a', $aggregate_command);
232
        }
233
        $processor->setFilter($filter);
234
 
235
        try {
236
            $return = $processor->execute();
237
        } catch (\Exception $e) {
238
            $this->error(503, $e->getMessage());
239
        }
240
 
241
        return $return;
242
    }
243
 
244
    /**
245
     * Get data to build a graph.
246
     *
247
     * @return array|string
248
     */
249
    public function graph(
250
        int $datestart,
251
        int $dateend,
252
        array $sources,
253
        array $protocols,
254
        array $ports,
255
        string $type,
256
        string $display,
257
    ) {
258
        $graph = Config::$db->get_graph_data($datestart, $dateend, $sources, $protocols, $ports, $type, $display);
259
        if (!\is_array($graph)) {
260
            $this->error(400, $graph);
261
        }
262
 
263
        return $graph;
264
    }
265
 
266
    public function graph_stats(): void {}
267
 
268
    /**
269
     * Get config info.
270
     *
271
     * @return array
272
     */
273
    public function config() {
274
        $sources = Config::$cfg['general']['sources'];
275
        $ports = Config::$cfg['general']['ports'];
276
        $frontend = Config::$cfg['frontend'];
277
 
278
        $stored_output_formats = []; // todo implement
279
        $stored_filters =  Config::$cfg['general']['filters'];
280
 
281
        $folder = \dirname(__FILE__, 2);
282
        $pidfile = $folder . '/nfsen-ng.pid';
283
        $daemon_running = file_exists($pidfile);
284
 
285
        // get date of first data point
286
        $firstDataPoint = \PHP_INT_MAX;
287
        foreach ($sources as $source) {
288
            $firstDataPoint = min($firstDataPoint, Config::$db->date_boundaries($source)[0]);
289
        }
290
        $frontend['data_start'] = $firstDataPoint;
291
 
292
        return [
293
            'sources' => $sources,
294
            'ports' => $ports,
295
            'stored_output_formats' => $stored_output_formats,
296
            'stored_filters' => $stored_filters,
297
            'daemon_running' => $daemon_running,
298
            'frontend' => $frontend,
299
            'version' => Config::VERSION,
300
            'tz_offset' => (new \DateTimeZone(date_default_timezone_get()))->getOffset(new \DateTime('now', new \DateTimeZone('UTC'))) / 3600,
301
        ];
302
    }
303
 
304
    /**
305
     * executes the host command with a timeout of 5 seconds.
306
     */
307
    public function host(string $ip): string {
308
        try {
309
            // check ip format
310
            if (!filter_var($ip, \FILTER_VALIDATE_IP)) {
311
                $this->error(400, 'Invalid IP address');
312
            }
313
 
314
            exec('host -W 5 ' . $ip, $output, $return_var);
315
            if ($return_var !== 0) {
316
                $this->error(404, 'Host command failed');
317
            }
318
 
319
            $output = implode(' ', $output);
320
            if (!preg_match('/domain name pointer (.*)\./', $output, $matches)) {
321
                $this->error(500, "Could not parse host output: {$output}");
322
            }
323
 
324
            return $matches[1];
325
        } catch (\Exception $e) {
326
            $this->error(500, $e->getMessage());
327
        }
328
    }
329
}