Passed
Push — develop ( 1d7aa0...7f16fa )
by Nikolay
35:31
created

Fail2BanConf::initProperty()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 27
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 27
rs 9.2888
c 0
b 0
f 0
cc 5
nc 2
nop 0
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright (C) 2017-2020 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System\Configs;
21
22
use MikoPBX\Common\Models\Fail2BanRules;
23
use MikoPBX\Common\Models\NetworkFilters;
24
use MikoPBX\Common\Models\PbxSettings;
25
use MikoPBX\Core\System\Processes;
26
use MikoPBX\Core\System\System;
27
use MikoPBX\Core\System\Util;
28
use MikoPBX\Core\System\Verify;
29
use MikoPBX\Modules\Config\ConfigClass;
30
use Phalcon\Di\Injectable;
31
use Phalcon\Text;
32
use SQLite3;
33
34
class Fail2BanConf extends Injectable
35
{
36
    private const FILTER_PATH     = '/etc/fail2ban/filter.d';
37
    private const ACTION_PATH     = '/etc/fail2ban/action.d';
38
    private const JAILS_DIR       = '/etc/fail2ban/jail.d';
39
    private const PID_FILE        = '/var/run/fail2ban/fail2ban.pid';
40
    public const FAIL2BAN_DB_PATH = '/var/lib/fail2ban/fail2ban.sqlite3';
41
42
    public bool $fail2ban_enable;
43
    private array $allPbxSettings;
44
45
    /**
46
     * Fail2Ban constructor.
47
     */
48
    public function __construct()
49
    {
50
        $this->allPbxSettings  = PbxSettings::getAllPbxSettings();
51
        $fail2ban_enable       = $this->allPbxSettings['PBXFail2BanEnabled'];
52
        $this->fail2ban_enable = ($fail2ban_enable === '1');
53
    }
54
55
    /**
56
     * Check fail2ban service and restart it died
57
     */
58
    public static function checkFail2ban(): void
59
    {
60
        $fail2ban = new self();
61
        if ($fail2ban->fail2ban_enable
62
            && ! $fail2ban->fail2banIsRunning()) {
63
            $fail2ban->fail2banStart();
64
        }
65
    }
66
67
    /**
68
     * Check fail2ban service status
69
     *
70
     * @return bool
71
     */
72
    private function fail2banIsRunning(): bool
73
    {
74
        $fail2banPath = Util::which('fail2ban-client');
75
        $res_ping     = Processes::mwExec("{$fail2banPath} ping");
76
        $res_stat     = Processes::mwExec("{$fail2banPath} status");
77
78
        $result = false;
79
        if ($res_ping === 0 && $res_stat === 0) {
80
            $result = true;
81
        }
82
83
        return $result;
84
    }
85
86
    /**
87
     * Start fail2ban service
88
     */
89
    public function fail2banStart(): void
90
    {
91
        if (Util::isSystemctl() && ! Util::isDocker()) {
92
            $systemctlPath = Util::which('systemctl');
93
            Processes::mwExec("{$systemctlPath} restart fail2ban");
94
95
            return;
96
        }
97
        Processes::killByName('fail2ban-server');
98
        $fail2banPath = Util::which('fail2ban-client');
99
        $cmd_start    = "{$fail2banPath} -x start";
100
        $command      = "($cmd_start;) > /dev/null 2>&1 &";
101
        Processes::mwExec($command);
102
    }
103
104
    /**
105
     * Applies iptables settings and restart firewall
106
     */
107
    public static function reloadFail2ban(): void
108
    {
109
        $fail2banPath = Util::which('fail2ban-client');
110
        if(file_exists(self::PID_FILE)){
111
            $pid = file_get_contents(self::PID_FILE);
112
        }else{
113
            $pid = Processes::getPidOfProcess('fail2ban-server');
114
        }
115
        $fail2ban = new self();
116
        if($fail2ban->fail2ban_enable && !empty($pid)){
117
            $fail2ban->generateModulesFilters();
118
            $fail2ban->generateModulesJailsLocal();
119
            // Перезагрузка конфигов без рестарта конфига.
120
            Processes::mwExecBg("{$fail2banPath} reload");
121
        }
122
    }
123
124
    /**
125
     * Checks whether BANS table exists in DB or not
126
     *
127
     * @param SQLite3 $db
128
     *
129
     * @return bool
130
     */
131
    public function tableBanExists(SQLite3 $db): bool
132
    {
133
        $q_check      = 'SELECT name FROM sqlite_master WHERE type = "table" AND name="bans"';
134
        $result_check = $db->query($q_check);
135
136
        return (false !== $result_check && $result_check->fetchArray(SQLITE3_ASSOC) !== false);
137
    }
138
139
    /**
140
     * Shutdown fail2ban service
141
     */
142
    public function fail2banStop(): void
143
    {
144
        if (Util::isSystemctl() && ! Util::isDocker()) {
145
            $systemctlPath = Util::which('systemctl');
146
            Processes::mwExec("{$systemctlPath} stop fail2ban");
147
        } else {
148
            $fail2banPath = Util::which('fail2ban-client');
149
            Processes::mwExec("{$fail2banPath} -x stop");
150
        }
151
    }
152
153
    /**
154
     * Create fail2ban dirs and DB if it does not exists
155
     *
156
     * @return string
157
     */
158
    public function fail2banMakeDirs(): string
159
    {
160
        $res_file = self::FAIL2BAN_DB_PATH;
161
        $filename = basename($res_file);
162
163
        $old_dir_db = '/cf/fail2ban';
164
        $dir_db     = $this->di->getShared('config')->path('core.fail2banDbDir');
165
        if (empty($dir_db)) {
166
            $dir_db = '/var/spool/fail2ban';
167
        }
168
        Util::mwMkdir($dir_db);
169
        // Создаем рабочие каталоги.
170
        $db_bd_dir = dirname($res_file);
171
        Util::mwMkdir($db_bd_dir);
172
173
        $create_link = false;
174
175
        // Символическая ссылка на базу данных.
176
        if (file_exists($res_file)){
177
            if (filetype($res_file) !== 'link') {
178
                unlink($res_file);
179
                $create_link = true;
180
            } elseif (readlink($res_file) === "$old_dir_db/$filename") {
181
                unlink($res_file);
182
                $create_link = true;
183
                if (file_exists("$old_dir_db/$filename")) {
184
                    // Перемещаем файл в новое местоположение.
185
                    $mvPath = Util::which('mv');
186
                    Processes::mwExec("{$mvPath} '$old_dir_db/$filename' '$dir_db/$filename'");
187
                }
188
            }
189
        }else{
190
            $sqlite3Path = Util::which('sqlite3');
191
            Processes::mwExec("{$sqlite3Path} {$dir_db}/{$filename} 'vacuum'");
192
            $create_link = true;
193
        }
194
195
        if ($create_link === true) {
196
            Util::createUpdateSymlink("$dir_db/$filename", $res_file);
197
        }
198
199
        return $res_file;
200
    }
201
202
    /**
203
     * Записываем конфиг для fail2ban. Описываем правила блокировок.
204
     */
205
    public function writeConfig(): void
206
    {
207
        [$max_retry, $find_time, $ban_time, $user_whitelist] = $this->initProperty();
208
        $this->generateActions();
209
        $this->generateJails();
210
211
        $httpPorts = [
212
            $this->allPbxSettings['WEBPort'],
213
            $this->allPbxSettings['WEBHTTPSPort']
214
        ];
215
        $sshPort = [
216
            $this->allPbxSettings['SSHPort'],
217
        ];
218
        $asteriskPorts = [
219
            $this->allPbxSettings['SIPPort'],
220
            $this->allPbxSettings['TLS_PORT'],
221
            $this->allPbxSettings['IAXPort'],
222
            $this->allPbxSettings['RTPPortFrom'].':'.$this->allPbxSettings['RTPPortTo'],
223
            $this->allPbxSettings['AJAMPortTLS']
224
        ];
225
        $asteriskAMI = [
226
            $this->allPbxSettings['AMIPort'],
227
            $this->allPbxSettings['AJAMPort'],
228
        ];
229
230
        $jails        = [
231
            'dropbear'    => 'miko-iptables-multiport-all[name=SSH, port="'.implode(',', $sshPort).'"]',
232
            'mikopbx-www' => 'miko-iptables-multiport-all[name=HTTP, port="'.implode(',', $httpPorts).'"]',
233
        ];
234
235
        $this->generateModulesJailsLocal($max_retry, $find_time, $ban_time);
236
237
        $config       = "[DEFAULT]\n" .
238
            "ignoreip = 127.0.0.1 {$user_whitelist}\n\n";
239
240
        $syslog_file = SyslogConf::getSyslogFile();
241
242
        foreach ($jails as $jail => $action) {
243
            $config .= "[{$jail}]\n" .
244
                "enabled = true\n" .
245
                "logpath = {$syslog_file}\n" .
246
                "maxretry = {$max_retry}\n" .
247
                "findtime = {$find_time}\n" .
248
                "bantime = {$ban_time}\n" .
249
                "logencoding = utf-8\n" .
250
                "action = {$action}\n\n";
251
        }
252
253
        $log_dir = System::getLogDir() . '/asterisk/';
254
        $config  .= "[asterisk_security_log]\n" .
255
            "enabled = true\n" .
256
            "filter = asterisk-main\n" .
257
            'action = miko-iptables-multiport-all[name=ASTERISK, port="'.implode(',', $asteriskPorts).'"]'. PHP_EOL.
258
            "logencoding = utf-8\n" .
259
            "maxretry = {$max_retry}\n" .
260
            "findtime = {$find_time}\n" .
261
            "bantime = {$ban_time}\n" .
262
            "logpath = {$log_dir}security_log\n\n";
263
264
        $config .= "[asterisk_error]\n" .
265
            "enabled = true\n" .
266
            "filter = asterisk-main\n" .
267
            'action = miko-iptables-multiport-all[name=ASTERISK_ERROR, port="'.implode(',', $asteriskPorts).'"]'. PHP_EOL.
268
            "maxretry = {$max_retry}\n" .
269
            "findtime = {$find_time}\n" .
270
            "bantime = {$ban_time}\n" .
271
            "logencoding = utf-8\n" .
272
            "logpath = {$log_dir}error\n\n";
273
274
        $config .= "[asterisk_public]\n" .
275
            "enabled = true\n" .
276
            "filter = asterisk-main\n" .
277
            'action = miko-iptables-multiport-all[name=ASTERISK_PUBLIC, port="'.implode(',', $asteriskPorts).'"]'. PHP_EOL.
278
            "maxretry = {$max_retry}\n" .
279
            "findtime = {$find_time}\n" .
280
            "bantime = {$ban_time}\n" .
281
            "logencoding = utf-8\n" .
282
            "logpath = {$log_dir}messages\n\n";
283
284
        $config .= "[asterisk_ami]\n" .
285
            "enabled = true\n" .
286
            "filter = asterisk-ami\n" .
287
            'action = miko-iptables-multiport-all[name=ASTERISK_AMI, port="'.implode(',', $asteriskAMI).'"]'. PHP_EOL.
288
            "maxretry = {$max_retry}\n" .
289
            "findtime = {$find_time}\n" .
290
            "bantime = {$ban_time}\n" .
291
            "logencoding = utf-8\n" .
292
            "logpath = {$log_dir}messages\n\n";
293
294
        Util::fileWriteContent('/etc/fail2ban/jail.local', $config);
295
    }
296
297
    private function generateActions(): void
298
    {
299
        $path = self::ACTION_PATH;
300
        $conf = "[INCLUDES]".PHP_EOL.
301
                "before = iptables-common.conf".PHP_EOL.
302
                "[Definition]".PHP_EOL.
303
                "actionstart = <iptables> -N f2b-<name>".PHP_EOL.
304
                "              <iptables> -A f2b-<name> -j <returntype>".PHP_EOL.
305
                "              <iptables> -I <chain> -p tcp -m multiport --dports <port> -j f2b-<name>".PHP_EOL.
306
                "              <iptables> -I <chain> -p udp -m multiport --dports <port> -j f2b-<name>".PHP_EOL.
307
                "actionstop = <iptables> -D <chain> -p tcp -m multiport --dports <port> -j f2b-<name>".PHP_EOL.
308
                "             <iptables> -D <chain> -p udp -m multiport --dports <port> -j f2b-<name>".PHP_EOL.
309
                "             <actionflush>".PHP_EOL.
310
                "             <iptables> -X f2b-<name>".PHP_EOL.
311
                "actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \\t]'".PHP_EOL.
312
                "actionban = <iptables> -I f2b-<name> 1 -s <ip> -p tcp -m multiport --dports <port> -j <blocktype>".PHP_EOL.
313
                "            <iptables> -I f2b-<name> 1 -s <ip> -p udp -m multiport --dports <port> -j <blocktype>".PHP_EOL.
314
                "actionunban = <iptables> -D f2b-<name> -s <ip> -p tcp -m multiport --dports <port> -j <blocktype>".PHP_EOL.
315
                "              <iptables> -D f2b-<name> -s <ip> -p udp -m multiport --dports <port> -j <blocktype>".PHP_EOL.
316
                "[Init]".PHP_EOL.PHP_EOL;
317
        file_put_contents("{$path}/miko-iptables-multiport-all.conf", $conf);
318
    }
319
320
321
    /**
322
     * Creates additional rules
323
     */
324
    private function generateJails(): void
325
    {
326
        $filterPath = self::FILTER_PATH;
327
328
        $conf = "[INCLUDES]\n" .
329
            "before = common.conf\n" .
330
            "[Definition]\n" .
331
            "_daemon = [\S\W\s]+web_auth\n" .
332
            'failregex = \sFrom:\s<HOST>\sUserAgent:(\S|\s)*Wrong password$' . "\n" .
333
            '            ^(\S|\s)*nginx:\s+\d+/\d+/\d+\s+(\S|\s)*status\s+403(\S|\s)*client:\s+<HOST>(\S|\s)*' . "\n" .
334
            '            \[error\] \d+#\d+: \*\d+ user "(?:[^"]+|.*?)":? (?:password mismatch|was not found in "[^\"]*"), client: <HOST>, server: \S*, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(?:, referrer: "\S+")?\s*$' . "\n" .
335
            "ignoreregex =\n";
336
        file_put_contents("{$filterPath}/mikopbx-www.conf", $conf);
337
338
        $conf = "[INCLUDES]\n" .
339
            "before = common.conf\n" .
340
            "[Definition]\n" .
341
            "_daemon = (authpriv.warn )?dropbear\n" .
342
            'prefregex = ^%(__prefix_line)s<F-CONTENT>(?:[Ll]ogin|[Bb]ad|[Ee]xit).+</F-CONTENT>$' . "\n" .
343
            'failregex = ^[Ll]ogin attempt for nonexistent user (\'.*\' )?from <HOST>:\d+$' . "\n" .
344
            '            ^[Bb]ad (PAM )?password attempt for .+ from <HOST>(:\d+)?$' . "\n" .
345
            '            ^[Ee]xit before auth \(user \'.+\', \d+ fails\): Max auth tries reached - user \'.+\' from <HOST>:\d+\s*$' . "\n" .
346
            "ignoreregex =\n";
347
        file_put_contents("{$filterPath}/dropbear.conf", $conf);
348
349
        $conf = "[INCLUDES]".PHP_EOL.
350
                "before = common.conf".PHP_EOL.PHP_EOL.
351
                "[Definition]".PHP_EOL.PHP_EOL.
352
                "_daemon = asterisk".PHP_EOL.PHP_EOL.
353
                "__pid_re = (?:\[\d+\])".PHP_EOL.
354
                "_c_ooooo = (\[C-\d+[a-z]*\]?)".PHP_EOL.PHP_EOL.
355
                "log_prefix= (?:NOTICE|SECURITY)%(__pid_re)s:?%(_c_ooooo)s?:? \S+:\d*( in \w+:)?".PHP_EOL.PHP_EOL.
356
                "failregex = ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host <HOST> failed to authenticate as '[^']*'\$".PHP_EOL.
357
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s(?:\sHost)? <HOST> failed to authenticate as '[^']*'\$".PHP_EOL.PHP_EOL.
358
                "ignoreregex =".PHP_EOL.PHP_EOL;
359
        file_put_contents("{$filterPath}/asterisk-ami.conf", $conf);
360
361
        $conf = "[INCLUDES]".PHP_EOL.
362
                "before = common.conf".PHP_EOL.PHP_EOL.
363
                "[Definition]".PHP_EOL.PHP_EOL.
364
                "_daemon = asterisk".PHP_EOL.PHP_EOL.
365
                "__pid_re = (?:\[\d+\])".PHP_EOL.
366
                "_c_ooooo = (\[C-\d+[a-z]*\]?)".PHP_EOL.PHP_EOL.
367
                "log_prefix= (?:NOTICE|SECURITY)%(__pid_re)s:?%(_c_ooooo)s?:? \S+:\d*( in \w+:)?".PHP_EOL.PHP_EOL.
368
                "failregex = ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Registration from '[^']*' failed for '<HOST>(:\d+)?' - (Wrong password|Username/auth name mismatch|No matching peer found|Not a local domain|Device does not match ACL|Peer is not supposed to register|ACL error \(permit/deny\)|Not a local domain)\$".PHP_EOL.
369
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s No registration for peer '[^']*' \(from <HOST>\)\$".PHP_EOL.
370
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host <HOST> failed MD5 authentication for '[^']*' \([^)]+\)\$".PHP_EOL.
371
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Failed to authenticate (user|device) [^@]+@<HOST>\S*\$".PHP_EOL.
372
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s (?:handle_request_subscribe: )?Sending fake auth rejection for (device|user) \d*<sip:[^@]+@<HOST>>;tag=\w+\S*\$".PHP_EOL.
373
                '            ^(%(__prefix_line)s|\[\]\s*WARNING%(__pid_re)s:?(?:\[C-[\da-f]*\])? )Ext\. s: "Rejecting unknown SIP connection from <HOST>"$'.PHP_EOL.
374
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s <HOST> tried to authenticate with nonexistent user".PHP_EOL.
375
                "			^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Request (?:'[^']*' )?from '(?:[^']*|.*?)' failed for '<HOST>(?::\d+)?'\s\(callid: [^\)]*\) - (?:No matching endpoint found|Not match Endpoint(?: Contact)? ACL|(?:Failed|Error) to authenticate)\s*\$".PHP_EOL.
376
                "			^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s\s*(anonymous:\s)?Call\s(from)*\s*\((\w*:)?<HOST>:\d+\) to extension '\S*' rejected because extension not found in context 'public-direct-dial'\.\$".PHP_EOL.
377
                '            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s SecurityEvent="(?:FailedACL|InvalidAccountID|ChallengeResponseFailed|InvalidPassword)"(?:(?:,(?!RemoteAddress=)\w+="[^"]*")*|.*?),RemoteAddress="IPV[46]/[^/"]+/<HOST>/\d+"(?:,(?!RemoteAddress=)\w+="[^"]*")*$'.PHP_EOL.
378
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s ^hacking attempt detected '<HOST>'\$".PHP_EOL.PHP_EOL.
379
                'ignoreregex = Service="AMI"'.PHP_EOL.PHP_EOL;
380
        file_put_contents("{$filterPath}/asterisk-main.conf", $conf);
381
382
        $this->generateModulesFilters();
383
    }
384
385
    /**
386
     * Generate additional modules filter files
387
     */
388
    private function generateModulesFilters(): void
389
    {
390
        $filterPath        = self::FILTER_PATH;
391
        $rmPath            = Util::which('rm');
392
        Processes::mwExec("{$rmPath} -rf {$filterPath}/module_*.conf");
393
394
395
        // Add additional modules routes
396
        $configClassObj = new ConfigClass();
397
        $additionalModulesJails = $configClassObj->hookModulesMethodWithArrayResult(ConfigClass::GENERATE_FAIL2BAN_JAILS);
398
        foreach ($additionalModulesJails as $moduleUniqueId=>$moduleJailText) {
399
            $fileName = Text::uncamelize($moduleUniqueId,'_').'.conf';
400
            file_put_contents("{$filterPath}/{$fileName}", $moduleJailText);
401
        }
402
    }
403
404
    /**
405
     * @param $max_retry
406
     * @param $find_time
407
     * @param $ban_time
408
     */
409
    private function generateModulesJailsLocal($max_retry = 0, $find_time = 0, $ban_time = 0): void
410
    {
411
        if($max_retry === 0){
412
            [$max_retry, $find_time, $ban_time] = $this->initProperty();
413
        }
414
        if(!is_dir(self::JAILS_DIR)){
415
            Util::mwMkdir(self::JAILS_DIR);
416
        }
417
        $prefix = 'pbx_';
418
        $extension = 'conf';
419
        Processes::mwExec("rm -rf ".self::JAILS_DIR."/{$prefix}*.{$extension}");
420
        $syslog_file = SyslogConf::getSyslogFile();
421
422
        $configClassObj = new ConfigClass();
423
        $additionalModulesJails = $configClassObj->hookModulesMethodWithArrayResult(ConfigClass::GENERATE_FAIL2BAN_JAILS);
424
        foreach ($additionalModulesJails as $moduleUniqueId=>$moduleJailText) {
425
            $fileName = Text::uncamelize($moduleUniqueId,'_');
426
427
            $config = "[{$fileName}]\n" .
428
                "enabled = true\n" .
429
                "logpath = {$syslog_file}\n" .
430
                "maxretry = {$max_retry}\n" .
431
                "findtime = {$find_time}\n" .
432
                "bantime = {$ban_time}\n" .
433
                "logencoding = utf-8\n" .
434
                "action = iptables-allports[name={$moduleUniqueId}, protocol=all]\n\n";
435
436
            file_put_contents(self::JAILS_DIR."/{$prefix}{$fileName}.{$extension}", $config);
437
        }
438
    }
439
440
    /**
441
     * @return array
442
     */
443
    private function initProperty(): array{
444
        $user_whitelist = '';
445
        /** @var Fail2BanRules $res */
446
        $res = Fail2BanRules::findFirst("id = '1'");
447
        if ($res !== null) {
448
            $max_retry = $res->maxretry;
449
            $find_time = $res->findtime;
450
            $ban_time = $res->bantime;
451
            $whitelist = (string) $res->whitelist;
452
            $arr_whitelist = explode(' ', $whitelist);
453
            foreach ($arr_whitelist as $ip_string) {
454
                if (Verify::isIpAddress($ip_string)) {
455
                    $user_whitelist .= "$ip_string ";
456
                }
457
            }
458
            $net_filters = NetworkFilters::find("newer_block_ip = '1'");
459
            foreach ($net_filters as $filter) {
460
                $user_whitelist .= "{$filter->permit} ";
461
            }
462
463
            $user_whitelist = trim($user_whitelist);
464
        } else {
465
            $max_retry = '10';
466
            $find_time = '1800';
467
            $ban_time = '43200';
468
        }
469
        return array($max_retry, $find_time, $ban_time, $user_whitelist);
470
    }
471
472
}