Issues (2963)

LibreNMS/IRCBot.php (1 issue)

1
<?php
2
/*
3
 * Copyright (C) 2014  <[email protected]>
4
 * Modified and Relicensed by <[email protected]> under the expressed
5
 * permission by the Copyright-Holder <[email protected]>.
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 * */
20
21
namespace LibreNMS;
22
23
use LibreNMS\Authentication\LegacyAuth;
24
use LibreNMS\DB\Eloquent;
25
use LibreNMS\Enum\AlertState;
26
use LibreNMS\Util\Number;
27
use LibreNMS\Util\Time;
28
use Permissions;
29
30
class IRCBot
31
{
32
    private $config;
33
34
    private $user;
35
36
    private $last_activity = 0;
37
38
    private $data = '';
39
40
    private $authd = [];
41
42
    private $debug = false;
43
44
    private $server = '';
45
46
    private $port = '';
47
48
    private $ssl = false;
49
50
    private $pass = '';
51
52
    private $nick = 'LibreNMS';
53
54
    private $tempnick = null;
55
56
    private $chan = [];
57
58
    private $commands = [
59
        'auth',
60
        'quit',
61
        'listdevices',
62
        'device',
63
        'port',
64
        'down',
65
        'version',
66
        'status',
67
        'log',
68
        'help',
69
        'reload',
70
        'join',
71
    ];
72
73
    private $command = '';
74
75
    private $external = [];
76
77
    private $tick = 62500;
78
79
    private $j = 0;
80
81
    private $socket = [];
82
83
    private $floodcount = 0;
84
85
    private $max_retry = 5;
86
87
    private $nickwait;
88
89
    private $buff;
90
91
    private $tokens;
92
93
    public function __construct()
94
    {
95
        $this->log('Setting up IRC-Bot..');
96
97
        $this->config = Config::getAll();
98
        $this->debug = $this->config['irc_debug'];
99
        $this->config['irc_authtime'] = $this->config['irc_authtime'] ? $this->config['irc_authtime'] : 3;
100
        $this->max_retry = $this->config['irc_maxretry'];
101
        $this->server = $this->config['irc_host'];
102
        if ($this->config['irc_port'][0] == '+') {
103
            $this->ssl = true;
104
            $this->port = substr($this->config['irc_port'], 1);
105
        } else {
106
            $this->port = $this->config['irc_port'];
107
        }
108
109
        if ($this->config['irc_nick']) {
110
            $this->nick = $this->config['irc_nick'];
111
        }
112
113
        if ($this->config['irc_alert_chan']) {
114
            if (strstr($this->config['irc_alert_chan'], ',')) {
115
                $this->config['irc_alert_chan'] = explode(',', $this->config['irc_alert_chan']);
116
            } elseif (! is_array($this->config['irc_alert_chan'])) {
117
                $this->config['irc_alert_chan'] = [$this->config['irc_alert_chan']];
118
            }
119
            $this->chan = $this->config['irc_alert_chan'];
120
        }
121
122
        if ($this->config['irc_pass']) {
123
            $this->pass = $this->config['irc_pass'];
124
        }
125
126
        $this->loadExternal();
127
        $this->log('Starting IRC-Bot..');
128
        $this->init();
129
    }
130
131
    //end __construct()
132
133
    private function loadExternal()
134
    {
135
        if (! $this->config['irc_external']) {
136
            return true;
137
        }
138
139
        $this->log('Caching external commands...');
140
        if (! is_array($this->config['irc_external'])) {
141
            $this->config['irc_external'] = explode(',', $this->config['irc_external']);
142
        }
143
144
        foreach ($this->config['irc_external'] as $ext) {
145
            $this->log("Command $ext...");
146
            if (($this->external[$ext] = file_get_contents('includes/ircbot/' . $ext . '.inc.php')) == '') {
147
                $this->log('failed!');
148
                unset($this->external[$ext]);
149
            }
150
        }
151
152
        return $this->log('Cached ' . sizeof($this->external) . ' commands.');
153
    }
154
155
    //end load_external()
156
157
    private function init()
158
    {
159
        if ($this->config['irc_alert']) {
160
            $this->connectAlert();
161
        }
162
163
        $this->last_activity = time();
164
165
        $this->j = 2;
166
167
        $this->connect();
168
        $this->log('Connected');
169
        if ($this->pass) {
170
            fwrite($this->socket['irc'], 'PASS ' . $this->pass . "\n\r");
171
        }
172
173
        $this->doAuth();
174
        $this->nickwait = 0;
175
        while (true) {
176
            foreach ($this->socket as $n => $socket) {
177
                if (! is_resource($socket) || feof($socket)) {
178
                    $this->log("Socket '$n' closed. Restarting.");
179
                    break 2;
180
                }
181
            }
182
183
            if (isset($this->tempnick)) {
184
                if ($this->nickwait > 100) {
185
                    $this->ircRaw('NICK ' . $this->nick);
186
                    $this->nickwait = 0;
187
                }
188
                $this->nickwait += 1;
189
            }
190
191
            $this->getData();
192
            if ($this->config['irc_alert']) {
193
                $this->alertData();
194
            }
195
196
            if ($this->config['irc_conn_timeout']) {
197
                $inactive_seconds = time() - $this->last_activity;
198
                $max_inactive = $this->config['irc_conn_timeout'];
199
                if ($inactive_seconds > $max_inactive) {
200
                    $this->log('No data from server since ' . $max_inactive . ' seconds. Restarting.');
201
                    break;
202
                }
203
            }
204
205
            usleep($this->tick);
206
        }
207
208
        return $this->init();
209
    }
210
211
    //end init()
212
213
    private function connectAlert()
214
    {
215
        $f = $this->config['install_dir'] . '/.ircbot.alert';
216
        if ((file_exists($f) && filetype($f) != 'fifo' && ! unlink($f)) || (! file_exists($f) && ! shell_exec("mkfifo $f && echo 1"))) {
217
            $this->log('Error - Cannot create Alert-File');
218
219
            return false;
220
        }
221
222
        if (($this->socket['alert'] = fopen($f, 'r+'))) {
223
            $this->log('Opened Alert-File');
224
            stream_set_blocking($this->socket['alert'], false);
225
226
            return true;
227
        }
228
229
        $this->log('Error - Cannot open Alert-File');
230
231
        return false;
232
    }
233
234
    //end connect_alert()
235
236
    private function read($buff)
237
    {
238
        $r = fread($this->socket[$buff], 8192);
239
        $this->buff[$buff] .= $r;
240
        $r = strlen($r);
241
        if (strstr($this->buff[$buff], "\n")) {
242
            $tmp = explode("\n", $this->buff[$buff], 2);
243
            $this->buff[$buff] = substr($this->buff[$buff], (strlen($tmp[0]) + 1));
244
            if ($this->debug) {
245
                $this->log("Returning buffer '$buff': '" . trim($tmp[0]) . "'");
246
            }
247
248
            return $tmp[0];
249
        }
250
251
        if ($this->debug && $r > 0) {
252
            $this->log("Expanding buffer '$buff' = '" . trim($this->buff[$buff]) . "'");
253
        }
254
255
        return false;
256
    }
257
258
    //end read()
259
260
    private function alertData()
261
    {
262
        if (($alert = $this->read('alert')) !== false) {
263
            $alert = json_decode($alert, true);
264
            if (! is_array($alert)) {
265
                return false;
266
            }
267
            if ($this->debug) {
268
                $this->log('Alert received ' . $alert['title']);
269
                $this->log('Alert state ' . $alert['state']);
270
                $this->log('Alert severity ' . $alert['severity']);
271
                $this->log('Alert channels ' . print_r($this->config['irc_alert_chan'], true));
0 ignored issues
show
Are you sure print_r($this->config['irc_alert_chan'], true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

271
                $this->log('Alert channels ' . /** @scrutinizer ignore-type */ print_r($this->config['irc_alert_chan'], true));
Loading history...
272
            }
273
274
            switch ($alert['state']) {
275
                case AlertState::WORSE:
276
                    $severity_extended = '+';
277
                    break;
278
                case AlertState::BETTER:
279
                    $severity_extended = '-';
280
                    break;
281
                default:
282
                    $severity_extended = '';
283
            }
284
            $severity = '';
285
            if (isset($alert['severity'])) {
286
                $severity = str_replace(['warning', 'critical', 'normal'], [$this->_color('Warning', 'yellow'), $this->_color('Critical', 'red'), $this->_color('Info', 'lightblue')], $alert['severity']) . $severity_extended . ' ';
287
            }
288
289
            if ($alert['state'] == AlertState::RECOVERED and $this->config['irc_alert_utf8']) {
290
                $severity = str_replace(['Warning', 'Critical'], ['̶W̶a̶r̶n̶i̶n̶g', '̶C̶r̶i̶t̶i̶c̶a̶l'], $severity);
291
            }
292
293
            if ($this->config['irc_alert_chan']) {
294
                foreach ($this->config['irc_alert_chan'] as $chan) {
295
                    $this->sendAlert($chan, $severity, $alert);
296
                    $this->ircRaw('BOTFLOODCHECK');
297
                }
298
            } else {
299
                foreach ($this->authd as $nick => $data) {
300
                    if ($data['expire'] >= time()) {
301
                        $this->sendAlert($nick, $severity, $alert);
302
                    }
303
                }
304
            }
305
        }
306
    }
307
308
    //end alertData()
309
310
    private function sendAlert($sendto, $severity, $alert)
311
    {
312
        $sendto = explode(' ', $sendto)[0];
313
        $this->ircRaw('PRIVMSG ' . $sendto . ' :' . $severity . trim($alert['title']));
314
        if ($this->config['irc_alert_short']) {
315
            // Only send the title if set to short
316
317
            return;
318
        }
319
320
        foreach (explode("\n", $alert['msg']) as $line) {
321
            $line = trim($line);
322
            if (strlen($line) < 1) {
323
                continue;
324
            }
325
            $line = $this->_html2irc($line);
326
            $line = strip_tags($line);
327
328
            // We don't need to repeat the title
329
            if (trim($line) != trim($alert['title'])) {
330
                $this->log("Sending alert $line");
331
                if ($this->config['irc_floodlimit'] > 100) {
332
                    $this->floodcount += strlen($line);
333
                } elseif ($this->config['irc_floodlimit'] > 1) {
334
                    $this->floodcount += 1;
335
                }
336
                if (($this->config['irc_floodlimit'] > 0) && ($this->floodcount > $this->config['irc_floodlimit'])) {
337
                    $this->log('Reached floodlimit ' . $this->floodcount);
338
                    $this->ircRaw('BOTFLOODCHECK');
339
                    sleep(2);
340
                    $this->floodcount = 0;
341
                }
342
                $this->ircRaw('PRIVMSG ' . $sendto . ' :' . $line);
343
            }
344
        }
345
    }
346
347
    //end sendAlert()
348
349
    private function getData()
350
    {
351
        if (($data = $this->read('irc')) !== false) {
352
            $this->last_activity = time();
353
            $this->data = $data;
354
            $ex = explode(' ', $this->data);
355
            if ($ex[0] == 'PING') {
356
                return $this->ircRaw('PONG ' . $ex[1]);
357
            }
358
359
            if ($ex[1] == 376 || $ex[1] == 422 || ($ex[1] == 'MODE' && $ex[2] == $this->nick)) {
360
                if ($this->j == 2) {
361
                    $this->joinChan();
362
                    $this->j = 0;
363
                }
364
            }
365
366
            if (($this->config['irc_ctcp']) && (preg_match('/^:' . chr(1) . '.*/', $ex[3]))) {
367
                // Handle CTCP
368
                $ctcp = trim(preg_replace('/[^A-Z]/', '', $ex[3]));
369
                $ctcp_reply = null;
370
                $this->log('Received irc CTCP: ' . $ctcp . ' from ' . $this->getUser($this->data));
371
                switch ($ctcp) {
372
                    case 'VERSION':
373
                        $ctcp_reply = chr(1) . "$ctcp " . $this->config['irc_ctcp_version'] . chr(1);
374
                        break;
375
                    case 'PING':
376
                        $ctcp_reply = chr(1) . "$ctcp " . $ex[4] . ' ' . $ex[5] . chr(1);
377
                        break;
378
                    case 'TIME':
379
                        $ctcp_reply = chr(1) . "$ctcp " . date('c') . chr(1);
380
                        break;
381
                }
382
                if ($ctcp_reply !== null) {
383
                    $this->log('Sending irc CTCP: ' . 'NOTICE ' . $this->getUser($this->data) . ' :' . $ctcp_reply);
384
385
                    return $this->ircRaw('NOTICE ' . $this->getUser($this->data) . ' :' . $ctcp_reply);
386
                }
387
            }
388
389
            if (($ex[1] == 'NICK') && (preg_replace('/^:/', '', $ex[2]) == $this->nick)) {
390
                // Nickname changed successfully
391
                if ($this->debug) {
392
                    $this->log('Regained our real nick');
393
                }
394
                unset($this->tempnick);
395
            }
396
            if (($ex[1] == 433) || ($ex[1] == 437)) {
397
                // Nickname already in use / temp unavailable
398
                if ($this->debug) {
399
                    $this->log('Nickname already in use...');
400
                }
401
                if ($ex[2] != '*') {
402
                    $this->tempnick = $ex[2];
403
                }
404
                if (! isset($this->tempnick)) {
405
                    $this->tempnick = $this->nick . rand(0, 99);
406
                }
407
                if ($this->debug) {
408
                    $this->log('Using temp nick ' . $this->tempnick);
409
                }
410
411
                return $this->ircRaw('NICK ' . $this->tempnick);
412
            }
413
            if ($ex[1] == 421) {
414
                // Unknown command
415
                if ($ex[3] == 'BOTFLOODCHECK') {
416
                    $this->floodcount = 0;
417
                }
418
            }
419
            $this->command = str_replace([chr(10), chr(13)], '', $ex[3]);
420
            if (strstr($this->command, ':.')) {
421
                $this->handleCommand();
422
            }
423
        }
424
    }
425
426
    //end getData()
427
428
    private function joinChan($chan = false)
429
    {
430
        if ($chan) {
431
            $this->chan[] = $chan;
432
        }
433
434
        foreach ($this->chan as $chan) {
435
            $this->ircRaw('JOIN ' . $chan);
436
        }
437
438
        return true;
439
    }
440
441
    //end joinChan()
442
443
    private function handleCommand()
444
    {
445
        $this->command = str_replace(':.', '', $this->command);
446
        $tmp = explode(':.' . $this->command . ' ', $this->data);
447
        $this->user = $this->getAuthdUser();
448
        $this->log('isAuthd-1? ' . $this->isAuthd());
449
        if (! $this->isAuthd() && (isset($this->config['irc_auth']))) {
450
            $this->hostAuth();
451
        }
452
        $this->log('isAuthd-2? ' . $this->isAuthd());
453
        if ($this->isAuthd() || trim($this->command) == 'auth') {
454
            $this->proceedCommand(str_replace("\n", '', trim($this->command)), trim($tmp[1]));
455
        }
456
457
        $this->authd[$this->getUser($this->data)] = $this->user;
458
459
        return false;
460
    }
461
462
    //end handleCommand()
463
464
    private function proceedCommand($command, $params)
465
    {
466
        $command = strtolower($command);
467
        if (in_array($command, $this->commands)) {
468
            $this->chkdb();
469
            $this->log($command . " ( '" . $params . "' )");
470
471
            return $this->{'_' . $command}($params);
472
        } elseif ($this->external[$command]) {
473
            $this->chkdb();
474
            $this->log($command . " ( '" . $params . "' ) [Ext]");
475
476
            return eval($this->external[$command]);
477
        }
478
479
        return false;
480
    }
481
482
    //end proceedCommand()
483
484
    private function respond($msg)
485
    {
486
        $chan = $this->getChan($this->data);
487
488
        return $this->sendMessage($msg, strstr($chan, '#') ? $chan : $this->getUser($this->data));
489
    }
490
491
    //end respond()
492
493
    private function getChan($param)
494
    {
495
        $data = explode('PRIVMSG ', $this->data, 3);
496
        $data = explode(' ', $data[1], 2);
497
498
        return $data[0];
499
    }
500
501
    //end getChan()
502
503
    private function getUser($param)
504
    {
505
        $arrData = explode('!', $param, 2);
506
507
        return str_replace(':', '', $arrData[0]);
508
    }
509
510
    //end getUser()
511
512
    private function getUserHost($param)
513
    {
514
        $arrData = explode(' ', $param, 2);
515
516
        return str_replace(':', '', $arrData[0]);
517
    }
518
519
    //end getUserHost()
520
521
    private function connect($try = 0)
522
    {
523
        if ($try > $this->max_retry) {
524
            $this->log('Failed too many connection attempts, aborting');
525
526
            return exit();
527
        }
528
529
        $this->log('Trying to connect (' . ($try + 1) . ') to ' . $this->server . ':' . $this->port . ($this->ssl ? ' (SSL)' : ''));
530
        if ($this->socket['irc']) {
531
            $this->ircRaw('QUIT :Reloading');
532
            fclose($this->socket['irc']);
533
        }
534
535
        if ($this->ssl) {
536
            $server = 'ssl://' . $this->server;
537
        } else {
538
            $server = $this->server;
539
        }
540
541
        if ($this->ssl && $this->config['irc_disable_ssl_check']) {
542
            $ssl_context_params = ['ssl'=>['allow_self_signed'=> true, 'verify_peer' => false, 'verify_peer_name' => false]];
543
            $ssl_context = stream_context_create($ssl_context_params);
544
            $this->socket['irc'] = stream_socket_client($server . ':' . $this->port, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $ssl_context);
545
        } else {
546
            $this->socket['irc'] = fsockopen($server, $this->port);
547
        }
548
549
        if (! is_resource($this->socket['irc'])) {
550
            if ($try < 5) {
551
                sleep(5);
552
            } elseif ($try < 10) {
553
                sleep(60);
554
            } else {
555
                sleep(300);
556
            }
557
558
            return $this->connect($try + 1);
559
        } else {
560
            stream_set_blocking($this->socket['irc'], false);
561
562
            return true;
563
        }
564
    }
565
566
    //end connect()
567
568
    private function doAuth()
569
    {
570
        if ($this->ircRaw('USER ' . $this->nick . ' 0 ' . $this->nick . ' :' . $this->nick) && $this->ircRaw('NICK ' . $this->nick)) {
571
            return true;
572
        }
573
574
        return false;
575
    }
576
577
    //end doAuth()
578
579
    private function sendMessage($message, $chan)
580
    {
581
        if ($this->debug) {
582
            $this->log("Sending 'PRIVMSG " . trim($chan) . ' :' . trim($message) . "'");
583
        }
584
585
        return $this->ircRaw('PRIVMSG ' . trim($chan) . ' :' . trim($message));
586
    }
587
588
    //end sendMessage()
589
590
    private function log($msg)
591
    {
592
        $log = '[' . date('r') . '] IRCbot ' . trim($msg) . "\n";
593
        echo $log;
594
        file_put_contents($this->config['log_dir'] . '/irc.log', $log, FILE_APPEND);
595
596
        return true;
597
    }
598
599
    //end log()
600
601
    private function chkdb()
602
    {
603
        if (! Eloquent::isConnected()) {
604
            try {
605
                Eloquent::boot();
606
            } catch (\PDOException $e) {
607
                $this->log('Cannot connect to MySQL: ' . $e->getMessage());
608
609
                return exit();
610
            }
611
        }
612
613
        return true;
614
    }
615
616
    //end chkdb()
617
618
    private function isAuthd()
619
    {
620
        if ($this->user['expire'] >= time()) {
621
            $this->user['expire'] = (time() + ($this->config['irc_authtime'] * 3600));
622
623
            return true;
624
        } else {
625
            return false;
626
        }
627
    }
628
629
    //end isAuthd()
630
631
    private function getAuthdUser()
632
    {
633
        return $this->authd[$this->getUser($this->data)];
634
    }
635
636
    //end getAuthUser()
637
638
    private function hostAuth()
639
    {
640
        $this->log('HostAuth');
641
        global $authorizer;
642
        foreach ($this->config['irc_auth'] as $nms_user => $hosts) {
643
            foreach ($hosts as $host) {
644
                $host = preg_replace("/\*/", '.*', $host);
645
                if ($this->debug) {
646
                    $this->log("HostAuth on irc matching $host to " . $this->getUserHost($this->data));
647
                }
648
                if (preg_match("/$host/", $this->getUserHost($this->data))) {
649
                    $user_id = LegacyAuth::get()->getUserid($nms_user);
650
                    $user = LegacyAuth::get()->getUser($user_id);
651
                    $this->user['name'] = $user['username'];
652
                    $this->user['id'] = $user_id;
653
                    $this->user['level'] = LegacyAuth::get()->getUserlevel($user['username']);
654
                    $this->user['expire'] = (time() + ($this->config['irc_authtime'] * 3600));
655
                    if ($this->user['level'] < 5) {
656
                        $this->user['devices'] = Permissions::devicesForUser($this->user['id'])->toArray();
657
                        $this->user['ports'] = Permissions::portsForUser($this->user['id'])->toArray();
658
                    }
659
                    if ($this->debug) {
660
                        $this->log("HostAuth on irc for '" . $user['username'] . "', ID: '" . $user_id . "', Host: '" . $host);
661
                    }
662
663
                    return true;
664
                }
665
            }
666
        }
667
668
        return false;
669
    }
670
671
    //end hostAuth
672
673
    private function ircRaw($params)
674
    {
675
        return fputs($this->socket['irc'], $params . "\r\n");
676
    }
677
678
    //end irc_raw()
679
680
    private function _auth($params)
681
    {
682
        global $authorizer;
683
        $params = explode(' ', $params, 2);
684
        if (strlen($params[0]) == 64) {
685
            if ($this->tokens[$this->getUser($this->data)] == $params[0]) {
686
                $this->user['expire'] = (time() + ($this->config['irc_authtime'] * 3600));
687
                $tmp_user = LegacyAuth::get()->getUser($this->user['id']);
688
                $tmp = LegacyAuth::get()->getUserlevel($tmp_user['username']);
689
                $this->user['level'] = $tmp;
690
                if ($this->user['level'] < 5) {
691
                    $this->user['devices'] = Permissions::devicesForUser($this->user['id'])->toArray();
692
                    $this->user['ports'] = Permissions::portsForUser($this->user['id'])->toArray();
693
                }
694
695
                return $this->respond('Authenticated.');
696
            } else {
697
                return $this->respond('Nope.');
698
            }
699
        } else {
700
            $user_id = LegacyAuth::get()->getUserid($params[0]);
701
            $user = LegacyAuth::get()->getUser($user_id);
702
            if ($user['email'] && $user['username'] == $params[0]) {
703
                $token = hash('gost', openssl_random_pseudo_bytes(1024));
704
                $this->tokens[$this->getUser($this->data)] = $token;
705
                $this->user['name'] = $params[0];
706
                $this->user['id'] = $user['user_id'];
707
                if ($this->debug) {
708
                    $this->log("Auth for '" . $params[0] . "', ID: '" . $user['user_id'] . "', Token: '" . $token . "', Mail: '" . $user['email'] . "'");
709
                }
710
711
                if (send_mail($user['email'], 'LibreNMS IRC-Bot Authtoken', "Your Authtoken for the IRC-Bot:\r\n\r\n" . $token . "\r\n\r\n") === true) {
712
                    return $this->respond('Token sent!');
713
                } else {
714
                    return $this->respond('Sorry, seems like mail doesnt like us.');
715
                }
716
            } else {
717
                return $this->respond('Who are you again?');
718
            }
719
        }//end if
720
    }
721
722
    //end _auth()
723
724
    private function _reload($params)
725
    {
726
        if ($this->user['level'] == 10) {
727
            if ($params == 'external') {
728
                $this->respond('Reloading external scripts.');
729
730
                return $this->loadExternal();
731
            }
732
            $new_config = Config::load();
733
            $this->respond('Reloading configuration & defaults');
734
            if ($new_config != $this->config) {
735
                $this->__construct();
736
737
                return;
738
            }
739
        } else {
740
            return $this->respond('Permission denied.');
741
        }
742
    }
743
744
    //end _reload()
745
746
    private function _join($params)
747
    {
748
        if ($this->user['level'] == 10) {
749
            return $this->joinChan($params);
750
        } else {
751
            return $this->respond('Permission denied.');
752
        }
753
    }
754
755
    //end _join()
756
757
    private function _quit($params)
758
    {
759
        if ($this->user['level'] == 10) {
760
            $this->ircRaw('QUIT :Requested');
761
762
            return exit();
763
        } else {
764
            return $this->respond('Permission denied.');
765
        }
766
    }
767
768
    //end _quit()
769
770
    private function _help($params)
771
    {
772
        $msg = join(', ', $this->commands);
773
        if (count($this->external) > 0) {
774
            $msg .= ', ' . join(', ', array_keys($this->external));
775
        }
776
777
        return $this->respond("Available commands: $msg");
778
    }
779
780
    //end _help()
781
782
    private function _version($params)
783
    {
784
        $versions = version_info();
785
        $schema_version = $versions['db_schema'];
786
        $version = $versions['local_ver'];
787
788
        $msg = $this->config['project_name'] . ', Version: ' . $version . ', DB schema: ' . $schema_version . ', PHP: ' . PHP_VERSION;
789
790
        return $this->respond($msg);
791
    }
792
793
    //end _version()
794
795
    private function _log($params)
796
    {
797
        $num = 1;
798
        $hostname = '';
799
        $params = explode(' ', $params);
800
        if ($params[0] > 1) {
801
            $num = $params[0];
802
        }
803
        if (strlen($params[1]) > 0) {
804
            $hostname = preg_replace("/[^A-z0-9\.\-]/", '', $params[1]);
805
        }
806
        $hostname = $hostname . '%';
807
        if ($this->user['level'] < 5) {
808
            $tmp = dbFetchRows('SELECT `event_id`, eventlog.device_id, devices.hostname, `datetime`,`message`, eventlog.type FROM `eventlog`, `devices` WHERE eventlog.device_id=devices.device_id and devices.hostname like "' . $hostname . '" and eventlog.device_id IN (' . implode(',', $this->user['devices']) . ') ORDER BY `event_id` DESC LIMIT ' . (int) $num);
809
        } else {
810
            $tmp = dbFetchRows('SELECT `event_id`, eventlog.device_id, devices.hostname, `datetime`,`message`, eventlog.type FROM `eventlog`, `devices` WHERE eventlog.device_id=devices.device_id and devices.hostname like "' . $hostname . '" ORDER BY `event_id` DESC LIMIT ' . (int) $num);
811
        }
812
813
        foreach ($tmp as $logline) {
814
            $response = $logline['datetime'] . ' ';
815
            $response .= $this->_color($logline['hostname'], null, null, 'bold') . ' ';
816
            if ($this->config['irc_alert_utf8']) {
817
                if (preg_match('/critical alert/', $logline['message'])) {
818
                    $response .= preg_replace('/critical alert/', $this->_color('critical alert', 'red'), $logline['message']) . ' ';
819
                } elseif (preg_match('/warning alert/', $logline['message'])) {
820
                    $response .= preg_replace('/warning alert/', $this->_color('warning alert', 'yellow'), $logline['message']) . ' ';
821
                } elseif (preg_match('/recovery/', $logline['message'])) {
822
                    $response .= preg_replace('/recovery/', $this->_color('recovery', 'green'), $logline['message']) . ' ';
823
                } else {
824
                    $response .= $logline['message'] . ' ';
825
                }
826
            } else {
827
                $response .= $logline['message'] . ' ';
828
            }
829
            if ($logline['type'] != 'NULL') {
830
                $response .= $logline['type'] . ' ';
831
            }
832
            if ($this->config['irc_floodlimit'] > 100) {
833
                $this->floodcount += strlen($response);
834
            } elseif ($this->config['irc_floodlimit'] > 1) {
835
                $this->floodcount += 1;
836
            }
837
            if (($this->config['irc_floodlimit'] > 0) && ($this->floodcount > $this->config['irc_floodlimit'])) {
838
                $this->ircRaw('BOTFLOODCHECK');
839
                sleep(2);
840
                $this->floodcount = 0;
841
            }
842
            $this->respond($response);
843
        }
844
845
        if (! $tmp) {
846
            $this->respond('Nothing to see, maybe a bug?');
847
        }
848
849
        return true;
850
    }
851
852
    //end _log()
853
854
    private function _down($params)
855
    {
856
        if ($this->user['level'] < 5) {
857
            $tmp = dbFetchRows('SELECT `hostname` FROM `devices` WHERE status=0 AND `device_id` IN (' . implode(',', $this->user['devices']) . ')');
858
        } else {
859
            $tmp = dbFetchRows('SELECT `hostname` FROM `devices` WHERE status=0');
860
        }
861
862
        $msg = '';
863
        foreach ($tmp as $db) {
864
            if ($db['hostname']) {
865
                $msg .= ', ' . $db['hostname'];
866
            }
867
        }
868
869
        $msg = substr($msg, 2);
870
        $msg = $msg ? $msg : 'Nothing to show :)';
871
872
        return $this->respond($msg);
873
    }
874
875
    //end _down()
876
877
    private function _device($params)
878
    {
879
        $params = explode(' ', $params);
880
        $hostname = $params[0];
881
        $device = dbFetchRow('SELECT * FROM `devices` WHERE `hostname` = ?', [$hostname]);
882
        if (! $device) {
883
            return $this->respond('Error: Bad or Missing hostname, use .listdevices to show all devices.');
884
        }
885
886
        if ($this->user['level'] < 5 && ! in_array($device['device_id'], $this->user['devices'])) {
887
            return $this->respond('Error: Permission denied.');
888
        }
889
890
        $status = $device['status'] ? 'Up ' . Time::formatInterval($device['uptime']) : 'Down';
891
        $status .= $device['ignore'] ? '*Ignored*' : '';
892
        $status .= $device['disabled'] ? '*Disabled*' : '';
893
894
        return $this->respond($device['os'] . ' ' . $device['version'] . ' ' . $device['features'] . ' ' . $status);
895
    }
896
897
    //end _device()
898
899
    private function _port($params)
900
    {
901
        $params = explode(' ', $params);
902
        $hostname = $params[0];
903
        $ifname = $params[1];
904
        if (! $hostname || ! $ifname) {
905
            return $this->respond('Error: Missing hostname or ifname.');
906
        }
907
908
        $device = dbFetchRow('SELECT * FROM `devices` WHERE `hostname` = ?', [$hostname]);
909
        $port = dbFetchRow('SELECT * FROM `ports` WHERE (`ifName` = ? OR `ifDescr` = ?) AND device_id = ?', [$ifname, $ifname, $device['device_id']]);
910
        if ($this->user['level'] < 5 && ! in_array($port['port_id'], $this->user['ports']) && ! in_array($device['device_id'], $this->user['devices'])) {
911
            return $this->respond('Error: Permission denied.');
912
        }
913
914
        $bps_in = Number::formatSi($port['ifInOctets_rate'] * 8, 2, 3, 'bps');
915
        $bps_out = Number::formatSi($port['ifOutOctets_rate'] * 8, 2, 3, 'bps');
916
        $pps_in = Number::formatBi($port['ifInUcastPkts_rate'], 2, 3, 'pps');
917
        $pps_out = Number::formatBi($port['ifOutUcastPkts_rate'], 2, 3, 'pps');
918
919
        return $this->respond($port['ifAdminStatus'] . '/' . $port['ifOperStatus'] . ' ' . $bps_in . ' > bps > ' . $bps_out . ' | ' . $pps_in . ' > PPS > ' . $pps_out);
920
    }
921
922
    //end _port()
923
924
    private function _listdevices($params)
925
    {
926
        if ($this->user['level'] < 5) {
927
            $tmp = dbFetchRows('SELECT `hostname` FROM `devices` WHERE `device_id` IN (' . implode(',', $this->user['devices']) . ')');
928
        } else {
929
            $tmp = dbFetchRows('SELECT `hostname` FROM `devices`');
930
        }
931
932
        $msg = '';
933
        foreach ($tmp as $device) {
934
            $msg .= ', ' . $device['hostname'];
935
        }
936
937
        $msg = substr($msg, 2);
938
        $msg = $msg ? $msg : 'Nothing to show..?';
939
940
        return $this->respond($msg);
941
    }
942
943
    //end _listdevices()
944
945
    private function _status($params)
946
    {
947
        $params = explode(' ', $params);
948
        $statustype = $params[0];
949
950
        $d_w = '';
951
        $d_a = '';
952
        $p_w = '';
953
        $p_a = '';
954
        if ($this->user['level'] < 5) {
955
            $d_w = ' WHERE device_id IN (' . implode(',', $this->user['devices']) . ')';
956
            $d_a = ' AND   device_id IN (' . implode(',', $this->user['devices']) . ')';
957
            $p_w = ' WHERE  port_id IN (' . implode(',', $this->user['ports']) . ') OR device_id IN (' . implode(',', $this->user['devices']) . ')';
958
            $p_a = ' AND (I.port_id IN (' . implode(',', $this->user['ports']) . ') OR I.device_id IN (' . implode(',', $this->user['devices']) . '))';
959
        }
960
961
        switch ($statustype) {
962
            case 'devices':
963
            case 'device':
964
            case 'dev':
965
                $devcount = dbFetchCell('SELECT count(*) FROM devices' . $d_w);
966
                $devup = dbFetchCell("SELECT count(*) FROM devices  WHERE status = '1' AND `ignore` = '0'" . $d_a);
967
                $devdown = dbFetchCell("SELECT count(*) FROM devices WHERE status = '0' AND `ignore` = '0'" . $d_a);
968
                $devign = dbFetchCell("SELECT count(*) FROM devices WHERE `ignore` = '1'" . $d_a);
969
                $devdis = dbFetchCell("SELECT count(*) FROM devices WHERE `disabled` = '1'" . $d_a);
970
                if ($devup > 0) {
971
                    $devup = $this->_color($devup, 'green');
972
                }
973
                if ($devdown > 0) {
974
                    $devdown = $this->_color($devdown, 'red');
975
                    $devcount = $this->_color($devcount, 'yellow', null, 'bold');
976
                } else {
977
                    $devcount = $this->_color($devcount, 'green', null, 'bold');
978
                }
979
                $msg = 'Devices: ' . $devcount . ' (' . $devup . ' up, ' . $devdown . ' down, ' . $devign . ' ignored, ' . $devdis . ' disabled' . ')';
980
                break;
981
982
            case 'ports':
983
            case 'port':
984
            case 'prt':
985
                $prtcount = dbFetchCell('SELECT count(*) FROM ports' . $p_w);
986
                $prtup = dbFetchCell("SELECT count(*) FROM ports AS I, devices AS D  WHERE I.ifOperStatus = 'up' AND I.ignore = '0' AND I.device_id = D.device_id AND D.ignore = '0'" . $p_a);
987
                $prtdown = dbFetchCell("SELECT count(*) FROM ports AS I, devices AS D WHERE I.ifOperStatus = 'down' AND I.ifAdminStatus = 'up' AND I.ignore = '0' AND D.device_id = I.device_id AND D.ignore = '0'" . $p_a);
988
                $prtsht = dbFetchCell("SELECT count(*) FROM ports AS I, devices AS D WHERE I.ifAdminStatus = 'down' AND I.ignore = '0' AND D.device_id = I.device_id AND D.ignore = '0'" . $p_a);
989
                $prtign = dbFetchCell("SELECT count(*) FROM ports AS I, devices AS D WHERE D.device_id = I.device_id AND (I.ignore = '1' OR D.ignore = '1')" . $p_a);
990
//                $prterr   = dbFetchCell("SELECT count(*) FROM ports AS I, devices AS D WHERE D.device_id = I.device_id AND (I.ignore = '0' OR D.ignore = '0') AND (I.ifInErrors_delta > '0' OR I.ifOutErrors_delta > '0')".$p_a);
991
                if ($prtup > 0) {
992
                    $prtup = $this->_color($prtup, 'green');
993
                }
994
                if ($prtdown > 0) {
995
                    $prtdown = $this->_color($prtdown, 'red');
996
                    $prtcount = $this->_color($prtcount, 'yellow', null, 'bold');
997
                } else {
998
                    $prtcount = $this->_color($prtcount, 'green', null, 'bold');
999
                }
1000
                $msg = 'Ports: ' . $prtcount . ' (' . $prtup . ' up, ' . $prtdown . ' down, ' . $prtign . ' ignored, ' . $prtsht . ' shutdown' . ')';
1001
                break;
1002
1003
            case 'services':
1004
            case 'service':
1005
            case 'srv':
1006
                $status_counts = [];
1007
                $status_colors = [0 => 'green', 3 => 'lightblue', 1 => 'yellow', 2 => 'red'];
1008
                $srvcount = dbFetchCell('SELECT COUNT(*) FROM services' . $d_w);
1009
                $srvign = dbFetchCell('SELECT COUNT(*) FROM services WHERE service_ignore = 1' . $d_a);
1010
                $srvdis = dbFetchCell('SELECT COUNT(*) FROM services WHERE service_disabled = 1' . $d_a);
1011
                $service_status = dbFetchRows("SELECT `service_status`, COUNT(*) AS `count` FROM `services` WHERE `service_disabled`=0 AND `service_ignore`=0 $d_a GROUP BY `service_status`");
1012
                $service_status = array_column($service_status, 'count', 'service_status'); // key by status
1013
1014
                foreach ($status_colors as $status => $color) {
1015
                    if (isset($service_status[$status])) {
1016
                        $status_counts[$status] = $this->_color($service_status[$status], $color);
1017
                        $srvcount = $this->_color($srvcount, $color, null, 'bold'); // upgrade the main count color
1018
                    } else {
1019
                        $status_counts[$status] = 0;
1020
                    }
1021
                }
1022
1023
                $msg = "Services: $srvcount ({$status_counts[0]} up, {$status_counts[2]} down, {$status_counts[1]} warning, {$status_counts[3]} unknown, $srvign ignored, $srvdis disabled)";
1024
                break;
1025
1026
            default:
1027
                $msg = 'Error: STATUS requires one of the following: <devices|device|dev>|<ports|port|prt>|<services|service|src>';
1028
                break;
1029
        }//end switch
1030
1031
        return $this->respond($msg);
1032
    }
1033
1034
    //end _status()
1035
1036
    private function _color($text, $fg_color, $bg_color = null, $other = null)
1037
    {
1038
        $colors = [
1039
            'white' => '00',
1040
            'black' => '01',
1041
            'blue' => '02',
1042
            'green' => '03',
1043
            'red' => '04',
1044
            'brown' => '05',
1045
            'purple' => '06',
1046
            'orange' => '07',
1047
            'yellow' => '08',
1048
            'lightgreen' => '09',
1049
            'cyan' => '10',
1050
            'lightcyan' => '11',
1051
            'lightblue' => '12',
1052
            'pink' => '13',
1053
            'grey' => '14',
1054
            'lightgrey' => '15',
1055
        ];
1056
        $ret = chr(3);
1057
        if (array_key_exists($fg_color, $colors)) {
1058
            $ret .= $colors[$fg_color];
1059
            if (array_key_exists($bg_color, $colors)) {
1060
                $ret .= ',' . $colors[$fg_color];
1061
            }
1062
        }
1063
        switch ($other) {
1064
            case 'bold':
1065
                $ret .= chr(2);
1066
                break;
1067
            case 'underline':
1068
                $ret .= chr(31);
1069
                break;
1070
            case 'italics':
1071
            case 'reverse':
1072
                $ret .= chr(22);
1073
                break;
1074
        }
1075
        $ret .= $text;
1076
        $ret .= chr(15);
1077
1078
        return $ret;
1079
    }
1080
1081
    // end _color
1082
1083
    private function _html2irc($string)
1084
    {
1085
        $string = urldecode($string);
1086
        $string = preg_replace('#<b>#i', chr(2), $string);
1087
        $string = preg_replace('#</b>#i', chr(2), $string);
1088
        $string = preg_replace('#<i>#i', chr(22), $string);
1089
        $string = preg_replace('#</i>#i', chr(22), $string);
1090
        $string = preg_replace('#<u>#i', chr(31), $string);
1091
        $string = preg_replace('#</u>#i', chr(31), $string);
1092
1093
        $colors = [
1094
            'white'     => '00',
1095
            'black'     => '01',
1096
            'blue'      => '02',
1097
            'green'     => '03',
1098
            'red'       => '04',
1099
            'brown'     => '05',
1100
            'purple'    => '06',
1101
            'orange'    => '07',
1102
            'yellow'    => '08',
1103
            'lightgreen' => '09',
1104
            'cyan'      => '10',
1105
            'lightcyan' => '11',
1106
            'lightblue' => '12',
1107
            'pink'      => '13',
1108
            'grey'      => '14',
1109
            'lightgrey' => '15',
1110
        ];
1111
1112
        foreach ($colors as $color => $code) {
1113
            $string = preg_replace("#<$color>#i", chr(3) . $code, $string);
1114
            $string = preg_replace("#</$color>#i", chr(3), $string);
1115
        }
1116
1117
        return $string;
1118
    }
1119
1120
    // end _html2irc
1121
}//end class
1122