Passed
Push — develop ( 3c5942...636dd4 )
by Nikolay
04:57
created

Firewall::fail2banUnban()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 33
rs 8.9777
c 0
b 0
f 0
cc 6
nc 4
nop 2
1
<?php
2
/**
3
 * Copyright © MIKO LLC - All Rights Reserved
4
 * Unauthorized copying of this file, via any medium is strictly prohibited
5
 * Proprietary and confidential
6
 * Written by Alexey Portnov, 4 2020
7
 */
8
9
namespace MikoPBX\Core\System;
10
11
use MikoPBX\Common\Models\{Fail2BanRules, FirewallRules, NetworkFilters};
12
use SQLite3;
13
14
class Firewall
15
{
16
    private $firewall_enable;
17
    private $fail2ban_enable;
18
19
    /**
20
     * Firewall constructor.
21
     */
22
    public function __construct()
23
    {
24
        $this->firewall_enable = false;
25
        $this->fail2ban_enable = false;
26
        // Получение настроек.
27
        $config = new MikoPBXConfig();
28
29
        $firewall_enable       = $config->getGeneralSettings('PBXFirewallEnabled');
30
        $this->firewall_enable = ($firewall_enable === '1');
31
32
        $fail2ban_enable       = $config->getGeneralSettings('PBXFail2BanEnabled');
33
        $this->fail2ban_enable = ($fail2ban_enable === '1');
34
    }
35
36
    /**
37
     * Рестарт firewall.
38
     *
39
     * @return array
40
     */
41
    public static function reloadFirewall(): array
42
    {
43
        $result = [];
44
45
        $pid_file = '/var/run/service_reloadFirewall.pid';
46
        if (file_exists($pid_file)) {
47
            $old_pid = file_get_contents($pid_file);
48
            $process = Util::getPidOfProcess("^{$old_pid}");
49
            if ($process !== '') {
50
                $result['result'] = 'ERROR';
51
                $result['data']   = 'Another restart process has not yet completed';
52
53
                return $result;
54
            }
55
        }
56
        file_put_contents($pid_file, getmypid());
57
58
        $firewall = new Firewall();
59
        $firewall->applyConfig();
60
61
        unlink($pid_file);
62
        $result['result'] = 'Success';
63
64
        return $result;
65
    }
66
67
    /**
68
     *    Установка правил Firewall.
69
     **/
70
    public function applyConfig(): void
71
    {
72
        self::fail2banStop();
73
        $this->dropAllRules();
74
        self::fail2banMakeDirs();
75
76
        if ($this->firewall_enable) {
77
            $arr_command   = [];
78
            $arr_command[] = $this->getIptablesInputRule('', '-m conntrack --ctstate ESTABLISHED,RELATED');
79
            // Добавляем разрешения на сервисы.
80
            $this->addFirewallRules($arr_command);
81
            // Все остальное запрещаем.
82
            $arr_command[] = $this->getIptablesInputRule('', '', 'DROP');
83
84
            // Кастомизация правил firewall.
85
            $arr_commands_custom = [];
86
            $out                 = [];
87
            Util::fileWriteContent('/etc/firewall_additional', '');
88
89
            $catPath = Util::which('cat');
90
            $grepPath = Util::which('grep');
91
            $busyboxPath = Util::which('busybox');
92
            $awkPath = Util::which('awk');
93
            Util::mwExec(
94
                "{$catPath} /etc/firewall_additional | {$grepPath} -v '|' | {$grepPath} -v '&'| {$grepPath} '^iptables' | {$busyboxPath} {$awkPath} -F ';' '{print $1}'",
95
                $arr_commands_custom
96
            );
97
            if (Util::isSystemctl()) {
98
                Util::mwMkdir('/etc/iptables');
99
                file_put_contents('/etc/iptables/iptables.mikopbx', implode("\n", $arr_command));
100
                file_put_contents(
101
                    '/etc/iptables/iptables.mikopbx',
102
                    "\n" . implode("\n", $arr_commands_custom),
103
                    FILE_APPEND
104
                );
105
                $systemctlPath = Util::which('systemctl');
106
                Util::mwExec("{$systemctlPath} restart mikopbx_iptables");
107
            } else {
108
                Util::mwExecCommands($arr_command, $out, 'firewall');
109
                Util::mwExecCommands($arr_commands_custom, $out, 'firewall_additional');
110
            }
111
        }
112
        if ($this->fail2ban_enable) {
113
            // Настройка правил бана.
114
            $this->writeConfig();
115
            self::fail2banStart();
116
        } else {
117
            self::fail2banStop();
118
        }
119
    }
120
121
    /**
122
     * Завершение работы fail2ban;
123
     */
124
    public static function fail2banStop(): void
125
    {
126
        if (Util::isSystemctl()) {
127
            $systemctlPath = Util::which('systemctl');
128
            Util::mwExec("{$systemctlPath} stop fail2ban");
129
        } else {
130
            $fail2banPath = Util::which('fail2ban-client');
131
            Util::mwExec("{$fail2banPath} -x stop");
132
        }
133
    }
134
135
    /**
136
     *    Удаление всех правил Firewall.
137
     */
138
    private function dropAllRules(): void
139
    {
140
        $iptablesPath = Util::which('iptables');
141
        Util::mwExec("{$iptablesPath} -F");
142
        Util::mwExec("{$iptablesPath} -X");
143
    }
144
145
    /**
146
     * Создает служебные директории и ссылки на файлы fail2ban
147
     *
148
     * @return string
149
     */
150
    public static function fail2banMakeDirs(): string
151
    {
152
        $res_file = self::fail2banGetDbPath();
153
        $filename = basename($res_file);
154
155
        $old_dir_db = '/cf/fail2ban';
156
        $dir_db     = self::getFail2banDbDir();
157
        Util::mwMkdir($dir_db);
158
        // Создаем рабочие каталоги.
159
        $db_bd_dir = dirname($res_file);
160
        Util::mwMkdir($db_bd_dir);
161
162
        $create_link = false;
163
164
        // Символическая ссылка на базу данных.
165
        if (@filetype($res_file) !== 'link') {
166
            @unlink($res_file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

166
            /** @scrutinizer ignore-unhandled */ @unlink($res_file);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
167
            $create_link = true;
168
        } elseif (readlink($res_file) === "$old_dir_db/$filename") {
169
            @unlink($res_file);
170
            $create_link = true;
171
            if (file_exists("$old_dir_db/$filename")) {
172
                // Перемещаем файл в новое местоположение.
173
                $mvPath = Util::which('mv');
174
                Util::mwExec("{$mvPath} '$old_dir_db/$filename' '$dir_db/$filename'");
175
            }
176
        }
177
178
        if ($create_link === true) {
179
            @symlink("$dir_db/$filename", $res_file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for symlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

179
            /** @scrutinizer ignore-unhandled */ @symlink("$dir_db/$filename", $res_file);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
180
        }
181
182
        return $res_file;
183
    }
184
185
    /**
186
     * Возвращает путь к файлу базы данных fail2ban.
187
     *
188
     * @return string
189
     */
190
    public static function fail2banGetDbPath(): string
191
    {
192
        return '/var/lib/fail2ban/fail2ban.sqlite3';
193
    }
194
195
    public static function getFail2banDbDir(): string
196
    {
197
        if (Storage::isStorageDiskMounted()) {
198
            $mount_point = Storage::getMediaDir();
199
            $db_dir      = "$mount_point/fail2ban";
200
        } else {
201
            $db_dir = "/var/spool/fail2ban";
202
        }
203
        Util::mwMkdir($db_dir);
204
        if ( ! file_exists($db_dir)) {
205
            $db_dir = '/tmp';
206
        }
207
208
        return $db_dir;
209
    }
210
211
    /**
212
     * Формирует строку правила iptables
213
     *
214
     * @param string $dport
215
     * @param string $other_data
216
     * @param string $action
217
     *
218
     * @return string
219
     */
220
    private function getIptablesInputRule($dport = '', $other_data = '', $action = 'ACCEPT'): string
221
    {
222
        $data_port = '';
223
        if (trim($dport) !== '') {
224
            $data_port = '--dport ' . $dport;
225
        }
226
        $other_data = trim($other_data);
227
228
        return "iptables -A INPUT $other_data $data_port -j $action";
229
    }
230
231
    /**
232
     * Генератор правил iptables.
233
     *
234
     * @param $arr_command
235
     */
236
    private function addFirewallRules(&$arr_command): void
237
    {
238
        /** @var \MikoPBX\Common\Models\FirewallRules $result */
239
        /** @var \MikoPBX\Common\Models\FirewallRules $rule */
240
        /** @var \MikoPBX\Common\Models\FirewallRules $rule */
241
        $result = FirewallRules::find();
242
        foreach ($result as $rule) {
243
            if ($rule->portfrom !== $rule->portto && trim($rule->portto) !== '') {
244
                $port = "{$rule->portfrom}:{$rule->portto}";
245
            } else {
246
                $port = $rule->portfrom;
247
            }
248
            /** @var \MikoPBX\Common\Models\NetworkFilters $network_filter */
249
            $network_filter = NetworkFilters::findFirst($rule->networkfilterid);
250
            if ($network_filter === null) {
251
                Util::sysLogMsg('Firewall', "network_filter_id not found {$rule->networkfilterid}");
252
                continue;
253
            }
254
            if ('0.0.0.0/0' === $network_filter->permit && $rule->action !== 'allow') {
255
                continue;
256
            }
257
            $other_data = "-p {$rule->protocol}";
258
            $other_data .= ($network_filter === null) ? '' : ' -s ' . $network_filter->permit;
259
            if ($rule->protocol === 'icmp') {
260
                $port       = '';
261
                $other_data .= ' --icmp-type echo-request';
262
            }
263
264
            $action        = ($rule->action === 'allow') ? 'ACCEPT' : 'DROP';
265
            $arr_command[] = $this->getIptablesInputRule($port, $other_data, $action);
266
        }
267
        // Разрешим все локальные подключения.
268
        $arr_command[] = $this->getIptablesInputRule('', '-s 127.0.0.1 ', 'ACCEPT');
269
    }
270
271
    /**
272
     * Записываем конфиг для fail2ban. Описываем правила блокировок.
273
     */
274
    private function writeConfig(): void
275
    {
276
        $user_whitelist = '';
277
        /** @var \MikoPBX\Common\Models\Fail2BanRules $res */
278
        $res = Fail2BanRules::findFirst("id = '1'");
279
        if ($res !== null) {
280
            $max_retry     = $res->maxretry;
281
            $find_time     = $res->findtime;
282
            $ban_time      = $res->bantime;
283
            $whitelist     = $res->whitelist;
284
            $arr_whitelist = explode(' ', $whitelist);
285
            foreach ($arr_whitelist as $ip_string) {
286
                if (Verify::isIpAddress($ip_string)) {
287
                    $user_whitelist .= "$ip_string ";
288
                }
289
            }
290
            $net_filters = NetworkFilters::find("newer_block_ip = '1'");
291
            foreach ($net_filters as $filter) {
292
                $user_whitelist .= "{$filter->permit} ";
293
            }
294
295
            $user_whitelist = trim($user_whitelist);
296
        } else {
297
            $max_retry = '10';
298
            $find_time = '1800';
299
            $ban_time  = '43200';
300
        }
301
        $this->generateJails();
302
        $jails  = [
303
            'dropbear'    => 'iptables-allports[name=SSH, protocol=all]',
304
            'mikoajam'    => 'iptables-allports[name=MIKOAJAM, protocol=all]',
305
            'mikopbx-www' => 'iptables-allports[name=HTTP, protocol=all]',
306
        ];
307
        $config = "[DEFAULT]\n" .
308
            "ignoreip = 127.0.0.1 {$user_whitelist}\n\n";
309
310
        $syslog_file = System::getSyslogFile();
311
312
        foreach ($jails as $jail => $action) {
313
            $config .= "[{$jail}]\n" .
314
                "enabled = true\n" .
315
                "backend = process\n" .
316
                "logpath = {$syslog_file}\n" .
317
                // "logprocess = logread -f\n".
318
                "maxretry = {$max_retry}\n" .
319
                "findtime = {$find_time}\n" .
320
                "bantime = {$ban_time}\n" .
321
                "logencoding = utf-8\n" .
322
                "action = {$action}\n\n";
323
        }
324
325
        $log_dir = System::getLogDir() . '/asterisk/';
326
        $config  .= "[asterisk_security_log]\n" .
327
            "enabled = true\n" .
328
            "filter = asterisk\n" .
329
            "action = iptables-allports[name=ASTERISK, protocol=all]\n" .
330
            "logencoding = utf-8\n" .
331
            "maxretry = {$max_retry}\n" .
332
            "findtime = {$find_time}\n" .
333
            "bantime = {$ban_time}\n" .
334
            "logpath = {$log_dir}security_log\n\n";
335
336
        $config .= "[asterisk_error]\n" .
337
            "enabled = true\n" .
338
            "filter = asterisk\n" .
339
            "action = iptables-allports[name=ASTERISK_ERROR, protocol=all]\n" .
340
            "maxretry = {$max_retry}\n" .
341
            "findtime = {$find_time}\n" .
342
            "bantime = {$ban_time}\n" .
343
            "logencoding = utf-8\n" .
344
            "logpath = {$log_dir}error\n\n";
345
346
        $config .= "[asterisk_public]\n" .
347
            "enabled = true\n" .
348
            "filter = asterisk\n" .
349
            "action = iptables-allports[name=ASTERISK_PUBLIC, protocol=all]\n" .
350
            "maxretry = {$max_retry}\n" .
351
            "findtime = {$find_time}\n" .
352
            "bantime = {$ban_time}\n" .
353
            "logencoding = utf-8\n" .
354
            "logpath = {$log_dir}messages\n\n";
355
356
        Util::fileWriteContent('/etc/fail2ban/jail.local', $config);
357
    }
358
359
    /**
360
     * Создаем дополнительные правила.
361
     */
362
    private function generateJails(): void
363
    {
364
        $conf = "[INCLUDES]\n" .
365
            "before = common.conf\n" .
366
            "[Definition]\n" .
367
            "_daemon = (authpriv.warn |auth.warn )?miko_ajam\n" .
368
            'failregex = ^%(__prefix_line)sFrom\s+<HOST>.\s+UserAgent:\s+[a-zA-Z0-9 \s\.,/:;\+\-_\)\(\[\]]*.\s+Fail\s+auth\s+http.$' . "\n" .
369
            '            ^%(__prefix_line)sFrom\s+<HOST>.\s+UserAgent:\s+[a-zA-Z0-9 \s\.,/:;\+\-_\)\(\[\]]*.\s+File\s+not\s+found.$' . "\n" .
370
            "ignoreregex =\n";
371
        file_put_contents('/etc/fail2ban/filter.d/mikoajam.conf', $conf);
372
373
        $conf = "[INCLUDES]\n" .
374
            "before = common.conf\n" .
375
            "[Definition]\n" .
376
            "_daemon = [\S\W\s]+web_auth\n" .
377
            'failregex = ^%(__prefix_line)sFrom:\s<HOST>\sUserAgent:(\S|\s)*Wrong password$' . "\n" .
378
            '            ^(\S|\s)*nginx:\s+\d+/\d+/\d+\s+(\S|\s)*status\s+403(\S|\s)*client:\s+<HOST>(\S|\s)*' . "\n" .
379
            "ignoreregex =\n";
380
        file_put_contents('/etc/fail2ban/filter.d/mikopbx-www.conf', $conf);
381
382
        $conf = "[INCLUDES]\n" .
383
            "before = common.conf\n" .
384
            "[Definition]\n" .
385
            "_daemon = (authpriv.warn )?dropbear\n" .
386
            'prefregex = ^%(__prefix_line)s<F-CONTENT>(?:[Ll]ogin|[Bb]ad|[Ee]xit).+</F-CONTENT>$' . "\n" .
387
            'failregex = ^[Ll]ogin attempt for nonexistent user (\'.*\' )?from <HOST>:\d+$' . "\n" .
388
            '            ^[Bb]ad (PAM )?password attempt for .+ from <HOST>(:\d+)?$' . "\n" .
389
            '            ^[Ee]xit before auth \(user \'.+\', \d+ fails\): Max auth tries reached - user \'.+\' from <HOST>:\d+\s*$' . "\n" .
390
            "ignoreregex =\n";
391
        file_put_contents('/etc/fail2ban/filter.d/dropbear.conf', $conf);
392
    }
393
394
    /**
395
     * Старт firewall.
396
     */
397
    public static function fail2banStart(): void
398
    {
399
        if (Util::isSystemctl()) {
400
            $systemctlPath = Util::which('systemctl');
401
            Util::mwExec("{$systemctlPath} restart fail2ban");
402
403
            return;
404
        }
405
        // Чистим битые строки, не улдаленные после отмены бана.
406
        self::cleanFail2banDb();
407
        Util::killByName('fail2ban-server');
408
        $fail2banPath = Util::which('fail2ban-client');
409
        $cmd_start = "{$fail2banPath} -x start";
410
        $command   = "($cmd_start;) > /dev/null 2>&1 &";
411
        Util::mwExec($command);
412
    }
413
414
    public static function cleanFail2banDb(): void
415
    {
416
        /** @var \MikoPBX\Common\Models\Fail2BanRules $res */
417
        $res = Fail2BanRules::findFirst("id = '1'");
418
        if ($res !== null) {
419
            $ban_time = $res->bantime;
420
        } else {
421
            $ban_time = '43800';
422
        }
423
        $path_db = self::fail2banGetDbPath();
424
        $db      = new SQLite3($path_db);
425
        $db->busyTimeout(3000);
426
        if (false === self::tableBanExists($db)) {
427
            return;
428
        }
429
        $q = 'DELETE' . ' from bans WHERE (timeofban+' . $ban_time . ')<' . time();
430
        $db->query($q);
431
    }
432
433
    /**
434
     * Проверка существования таблицы ban в базе данных.
435
     *
436
     * @param SQLite3 $db
437
     *
438
     * @return bool
439
     */
440
    public static function tableBanExists($db): bool
441
    {
442
        $q_check      = 'SELECT' . ' name FROM sqlite_master WHERE type = "table" AND name="bans"';
443
        $result_check = $db->query($q_check);
444
445
        return (false !== $result_check && $result_check->fetchArray(SQLITE3_ASSOC) !== false);
446
    }
447
448
    /**
449
     * Проверка запущен ли fail2ban.
450
     */
451
    public static function checkFail2ban(): void
452
    {
453
        $firewall = new Firewall();
454
        if ($firewall->fail2ban_enable && ! $firewall->fail2banIsRunning()) {
455
            self::fail2banStart();
456
        }
457
    }
458
459
    /**
460
     * Проверка статуса fail2ban
461
     *
462
     * @return bool
463
     */
464
    private function fail2banIsRunning(): bool
465
    {
466
        $fail2banPath = Util::which('fail2ban-client');
467
        $res_ping = Util::mwExec("{$fail2banPath} ping");
468
        $res_stat = Util::mwExec("{$fail2banPath} status");
469
470
        $result = false;
471
        if ($res_ping === 0 && $res_stat === 0) {
472
            $result = true;
473
        }
474
475
        return $result;
476
    }
477
478
    /**
479
     * Удалить адрес из бана.
480
     *
481
     * @param $ip
482
     *
483
     * @return array
484
     */
485
    public static function fail2banUnbanAll($ip): array
486
    {
487
        $ip     = trim($ip);
488
        $result = ['result' => 'Success'];
489
        if ( ! Verify::isIpAddress($ip)) {
490
            $result['message'] = "Not valid ip '{$ip}'.";
491
492
            return $result;
493
        }
494
        $config          = new MikoPBXConfig();
495
        $fail2ban_enable = $config->getGeneralSettings('PBXFail2BanEnabled');
496
        $enable          = ($fail2ban_enable == '1');
497
        if ($enable) {
498
            // Попробуем найти jail по данным DB.
499
            // fail2ban-client unban 172.16.156.1
500
            // TODO Util::mwExec("fail2ban-client unban {$ip}}");
501
            $data = self::getBanIp($ip);
502
            foreach ($data as $row) {
503
                $res = self::fail2banUnban($ip, $row['jail']);
504
                if ($res['result'] === 'ERROR') {
505
                    $result = $res;
506
                }
507
            }
508
        } else {
509
            $result = self::fail2banUnbanDb($ip);
510
        }
511
512
        return $result;
513
    }
514
515
    /**
516
     * Возвращает массив забаненных ip. Либо данные по конкретному адресу.
517
     *
518
     * @param null $ip
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $ip is correct as it would always require null to be passed?
Loading history...
519
     *
520
     * @return array
521
     */
522
    public static function getBanIp($ip = null): array
523
    {
524
        $result = [];
525
526
        /** @var \MikoPBX\Common\Models\Fail2BanRules $res */
527
        $res = Fail2BanRules::findFirst("id = '1'");
528
        if ($res !== null) {
529
            $ban_time = $res->bantime;
530
        } else {
531
            $ban_time = '43800';
532
        }
533
534
        // Добавленн фильтр по времени бана. возвращаем только адреса, которые еще НЕ разбанены.
535
        $q = 'SELECT' . ' DISTINCT jail,ip,MAX(timeofban) AS timeofban, MAX(timeofban+' . $ban_time . ') AS timeunban FROM bans where (timeofban+' . $ban_time . ')>' . time(
536
            );
537
        if ($ip !== null) {
0 ignored issues
show
introduced by
The condition $ip !== null is always false.
Loading history...
538
            $q .= " AND ip='{$ip}'";
539
        }
540
        $q .= ' GROUP BY jail,ip';
541
542
        $path_db = self::fail2banGetDbPath();
543
        $db      = new SQLite3($path_db);
544
        $db->busyTimeout(5000);
545
546
        if (false === self::tableBanExists($db)) {
547
            // Таблица не существует. Бана нет.
548
            return $result;
549
        }
550
551
        $results = $db->query($q);
552
        if (false !== $results && $results->numColumns() > 0) {
553
            while ($res = $results->fetchArray(SQLITE3_ASSOC)) {
554
                $result[] = $res;
555
            }
556
        }
557
558
        return $result;
559
    }
560
561
    /**
562
     * Удалить из бана конкретный IP из конкретного jail.
563
     *
564
     * @param $ip
565
     * @param $jail
566
     *
567
     * @return array
568
     */
569
    public static function fail2banUnban($ip, $jail): array
570
    {
571
        $res = ['result' => 'ERROR'];
572
        $ip  = trim($ip);
573
        // Валидация...
574
        $matches = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $matches is dead and can be removed.
Loading history...
575
        // preg_match_all('/^[a-z-]+$/', $jail, $matches, PREG_SET_ORDER);
576
        if ( ! Verify::isIpAddress($ip)) {
577
            // это НЕ IP адрес, или не корректно указан jail.
578
            $res['message'] = 'Not valid ip or jail.';
579
580
            return $res;
581
        }
582
        $path_sock = '/var/run/fail2ban/fail2ban.sock';
583
        if (file_exists($path_sock) && filetype($path_sock) === 'socket') {
584
            $out = [];
585
            $fail2banPath = Util::which('fail2ban-client');
586
            Util::mwExec("{$fail2banPath} set {$jail} unbanip {$ip} 2>&1", $out);
587
            $res_data = trim(implode('', $out));
588
            if ($res_data !== $ip && $res_data !== "IP $ip is not banned") {
589
                $res['message'] = 'Error fail2ban-client. ' . $res_data;
590
591
                return $res;
592
            }
593
        } else {
594
            $res['message'] = 'Fail2ban not run.';
595
596
            return $res;
597
        }
598
599
        $res = self::fail2banUnbanDb($ip, $jail);
600
601
        return $res;
602
    }
603
604
    /**
605
     * Удаление бана из базы.
606
     *
607
     * @param        $ip
608
     * @param string $jail
609
     *
610
     * @return array
611
     */
612
    public static function fail2banUnbanDb($ip, $jail = ''): array
613
    {
614
        $jail_q  = ($jail === '') ? '' : "AND jail = '{$jail}'";
615
        $path_db = self::fail2banGetDbPath();
616
        $db      = new SQLite3($path_db);
617
        $db->busyTimeout(3000);
618
        if (false === self::tableBanExists($db)) {
619
            // Таблица не существует. Бана нет.
620
            return [
621
                'result'  => 'Success',
622
                'message' => '',
623
            ];
624
        }
625
        $q = 'DELETE' . " FROM bans WHERE ip = '{$ip}' {$jail_q}";
626
        $db->query($q);
627
628
        $err = $db->lastErrorMsg();
629
630
        return [
631
            'result'  => ($err === 'not an error') ? 'Success' : 'ERROR',
632
            'message' => $err,
633
        ];
634
    }
635
636
}
637