Subversion Repositories ALCASAR

Rev

Details | Last modification | View Log

Rev Author Line No. Line
3241 rexy 1
<?php
2
 
3
namespace mbolli\nfsen_ng\processor;
4
 
5
use mbolli\nfsen_ng\common\Config;
6
use mbolli\nfsen_ng\common\Debug;
7
 
8
class Nfdump implements Processor {
9
    private array $cfg = [
10
        'env' => [],
11
        'option' => [],
12
        'format' => null,
13
        'filter' => [],
14
    ];
15
    private array $clean;
16
    private readonly Debug $d;
17
    public static ?self $_instance = null;
18
 
19
    public function __construct() {
20
        $this->d = Debug::getInstance();
21
        $this->clean = $this->cfg;
22
        $this->reset();
23
    }
24
 
25
    public static function getInstance(): self {
26
        if (!(self::$_instance instanceof self)) {
27
            self::$_instance = new self();
28
        }
29
 
30
        return self::$_instance;
31
    }
32
 
33
    /**
34
     * Sets an option's value.
35
     */
36
    public function setOption(string $option, $value): void {
37
        switch ($option) {
38
            case '-M': // set sources
39
                // only sources specified in settings allowed
40
                $queried_sources = explode(':', (string) $value);
41
                foreach ($queried_sources as $s) {
42
                    if (!\in_array($s, Config::$cfg['general']['sources'], true)) {
43
                        continue;
44
                    }
45
                    $this->cfg['env']['sources'][] = $s;
46
                }
47
 
48
                // cancel if no sources remain
49
                if (empty($this->cfg['env']['sources'])) {
50
                    break;
51
                }
52
 
53
                // set sources path
54
                $this->cfg['option'][$option] = implode(\DIRECTORY_SEPARATOR, [
55
                    $this->cfg['env']['profiles-data'],
56
                    $this->cfg['env']['profile'],
57
                    implode(':', $this->cfg['env']['sources']),
58
                ]);
59
 
60
                break;
61
            case '-R': // set path
62
                $this->cfg['option'][$option] = $this->convert_date_to_path($value[0], $value[1]);
63
                break;
64
            case '-o': // set output format
65
                $this->cfg['format'] = $value;
66
                break;
67
            default:
68
                $this->cfg['option'][$option] = $value;
69
                $this->cfg['option']['-o'] = 'csv'; // always get parsable data todo user-selectable? calculations bps/bpp/pps not in csv
70
                break;
71
        }
72
    }
73
 
74
    /**
75
     * Sets a filter's value.
76
     */
77
    public function setFilter(string $filter): void {
78
        $this->cfg['filter'] = $filter;
79
    }
80
 
81
    /**
82
     * Executes the nfdump command, tries to throw an exception based on the return code.
83
     *
84
     * @throws \Exception
85
     */
86
    public function execute(): array {
87
        $output = [];
88
        $processes = [];
89
        $return = '';
90
        $timer = microtime(true);
91
        $filter = (empty($this->cfg['filter'])) ? '' : ' ' . escapeshellarg((string) $this->cfg['filter']);
92
        $command = $this->cfg['env']['bin'] . ' ' . $this->flatten($this->cfg['option']) . $filter . ' 2>&1';
93
        $this->d->log('Trying to execute ' . $command, \LOG_DEBUG);
94
 
95
        // check for already running nfdump processes
96
        exec('ps -eo user,pid,args | grep -v grep | grep `whoami` | grep "' . $this->cfg['env']['bin'] . '"', $processes);
97
        if (\count($processes) / 2 > (int) Config::$cfg['nfdump']['max-processes']) {
98
            throw new \Exception('There already are ' . \count($processes) / 2 . ' processes of NfDump running!');
99
        }
100
 
101
        // execute nfdump
102
        exec($command, $output, $return);
103
 
104
        // prevent logging the command usage description
105
        if (isset($output[0]) && preg_match('/^usage/i', $output[0])) {
106
            $output = [];
107
        }
108
 
109
        switch ($return) {
110
            case 127:
111
                throw new \Exception('NfDump: Failed to start process. Is nfdump installed? <br><b>Output:</b> ' . implode(' ', $output));
112
            case 255:
113
                throw new \Exception('NfDump: Initialization failed. ' . $command . '<br><b>Output:</b> ' . implode(' ', $output));
114
            case 254:
115
                throw new \Exception('NfDump: Error in filter syntax. <br><b>Output:</b> ' . implode(' ', $output));
116
            case 250:
117
                throw new \Exception('NfDump: Internal error. <br><b>Output:</b> ' . implode(' ', $output));
118
        }
119
 
120
        // add command to output
121
        array_unshift($output, $command);
122
 
123
        // if last element contains a colon, it's not a csv
124
        if (str_contains($output[\count($output) - 1], ':')) {
125
            return $output; // return output if it is a flows/packets/bytes dump
126
        }
127
 
128
        // remove the 3 summary lines at the end of the csv output
129
        $output = \array_slice($output, 0, -3);
130
 
131
        // slice csv (only return the fields actually wanted)
132
        $field_ids_active = [];
133
        $parsed_header = false;
134
        $format = false;
135
        if (isset($this->cfg['format'])) {
136
            $format = $this->get_output_format($this->cfg['format']);
137
        }
138
 
139
        foreach ($output as $i => &$line) {
140
            if ($i === 0) {
141
                continue;
142
            } // skip nfdump command
143
            $line = str_getcsv($line, ',');
144
            $temp_line = [];
145
 
146
            if (\count($line) === 1 || preg_match('/limit/', $line[0]) || preg_match('/error/', $line[0])) { // probably an error message or warning. add to command
147
                $output[0] .= ' <br><b>' . $line[0] . '</b>';
148
                unset($output[$i]);
149
                continue;
150
            }
151
            if (!\is_array($format)) {
152
                $format = $line;
153
            } // set first valid line as header if not already defined
154
 
155
            foreach ($line as $field_id => $field) {
156
                // heading has the field identifiers. fill $fields_active with all active fields
157
                if ($parsed_header === false) {
158
                    if (\in_array($field, $format, true)) {
159
                        $field_ids_active[array_search($field, $format, true)] = $field_id;
160
                    }
161
                }
162
 
163
                // remove field if not in $fields_active
164
                if (\in_array($field_id, $field_ids_active, true)) {
165
                    $temp_line[array_search($field_id, $field_ids_active, true)] = $field;
166
                }
167
            }
168
 
169
            $parsed_header = true;
170
            ksort($temp_line);
171
            $line = array_values($temp_line);
172
        }
173
 
174
        // add execution time to output
175
        $output[0] .= '<br><b>Execution time:</b> ' . round(microtime(true) - $timer, 3) . ' seconds';
176
 
177
        return array_values($output);
178
    }
179
 
180
    /**
181
     * Concatenates key and value of supplied array.
182
     */
183
    private function flatten(array $array): string {
184
        $output = '';
185
 
186
        foreach ($array as $key => $value) {
187
            if ($value === null) {
188
                $output .= $key . ' ';
189
            } else {
190
                $output .= \is_int($key) ?: $key . ' ' . escapeshellarg((string) $value) . ' ';
191
            }
192
        }
193
 
194
        return $output;
195
    }
196
 
197
    /**
198
     * Reset config.
199
     */
200
    public function reset(): void {
201
        $this->clean['env'] = [
202
            'bin' => Config::$cfg['nfdump']['binary'],
203
            'profiles-data' => Config::$cfg['nfdump']['profiles-data'],
204
            'profile' => Config::$cfg['nfdump']['profile'],
205
            'sources' => [],
206
        ];
207
        $this->cfg = $this->clean;
208
    }
209
 
210
    /**
211
     * Converts a time range to a nfcapd file range
212
     * Ensures that files actually exist.
213
     *
214
     * @throws \Exception
215
     */
216
    public function convert_date_to_path(int $datestart, int $dateend): string {
217
        $start = new \DateTime();
218
        $end = new \DateTime();
219
        $start->setTimestamp((int) $datestart - ($datestart % 300));
220
        $end->setTimestamp((int) $dateend - ($dateend % 300));
221
        $filestart = $fileend = '-';
222
        $filestartexists = false;
223
        $fileendexists = false;
224
        $sourcepath = $this->cfg['env']['profiles-data'] . \DIRECTORY_SEPARATOR . $this->cfg['env']['profile'] . \DIRECTORY_SEPARATOR;
225
 
226
        // if start file does not exist, increment by 5 minutes and try again
227
        while ($filestartexists === false) {
228
            if ($start >= $end) {
229
                break;
230
            }
231
 
232
            foreach ($this->cfg['env']['sources'] as $source) {
233
                if (file_exists($sourcepath . $source . \DIRECTORY_SEPARATOR . $filestart)) {
234
                    $filestartexists = true;
235
                }
236
            }
237
 
238
            $pathstart = $start->format('Y/m/d') . \DIRECTORY_SEPARATOR;
239
            $filestart = $pathstart . 'nfcapd.' . $start->format('YmdHi');
240
            $start->add(new \DateInterval('PT5M'));
241
        }
242
 
243
        // if end file does not exist, subtract by 5 minutes and try again
244
        while ($fileendexists === false) {
245
            if ($end === $start) { // strict comparison won't work
246
                $fileend = $filestart;
247
                break;
248
            }
249
 
250
            foreach ($this->cfg['env']['sources'] as $source) {
251
                if (file_exists($sourcepath . $source . \DIRECTORY_SEPARATOR . $fileend)) {
252
                    $fileendexists = true;
253
                }
254
            }
255
 
256
            $pathend = $end->format('Y/m/d') . \DIRECTORY_SEPARATOR;
257
            $fileend = $pathend . 'nfcapd.' . $end->format('YmdHi');
258
            $end->sub(new \DateInterval('PT5M'));
259
        }
260
 
261
        return $filestart . \PATH_SEPARATOR . $fileend;
262
    }
263
 
264
    public function get_output_format($format): array {
265
        // todo calculations like bps/pps? flows? concatenate sa/sp to sap?
266
        return match ($format) {
267
            'line' => ['ts', 'td', 'pr', 'sa', 'sp', 'da', 'dp', 'ipkt', 'ibyt', 'fl'],
268
            'long' => ['ts', 'td', 'pr', 'sa', 'sp', 'da', 'dp', 'flg', 'stos', 'dtos', 'ipkt', 'ibyt', 'fl'],
269
            'extended' => ['ts', 'td', 'pr', 'sa', 'sp', 'da', 'dp', 'ipkt', 'ibyt', 'ibps', 'ipps', 'ibpp'],
270
            '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'],
271
            default => explode(' ', str_replace(['fmt:', '%'], '', (string) $format)),
272
        };
273
    }
274
}