FirewallController::deleteAction()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 18
rs 9.9
c 0
b 0
f 0
cc 4
nc 4
nop 1
1
<?php
2
3
/*
4
 * MikoPBX - free phone system for small business
5
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
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 along with this program.
18
 * If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
namespace MikoPBX\AdminCabinet\Controllers;
22
23
use MikoPBX\AdminCabinet\Forms\FirewallEditForm;
24
use MikoPBX\AdminCabinet\Library\Cidr;
25
use MikoPBX\Common\Models\{FirewallRules, LanInterfaces, NetworkFilters, PbxSettings};
26
27
class FirewallController extends BaseController
28
{
29
    /**
30
     * Prepares index page
31
     */
32
    public function indexAction(): void
33
    {
34
        $calculator        = new Cidr();
35
        $localAddresses    = [];
36
        $localAddresses[]  = '0.0.0.0/0';
37
        $conditions        = 'disabled=0 AND internet=0'; // We need only local networks here
38
        $networkInterfaces = LanInterfaces::find($conditions);
39
        foreach ($networkInterfaces as $interface) {
40
            if (empty($interface->ipaddr)) {
41
                continue;
42
            }
43
44
            if (!str_contains($interface->subnet, '.')) {
45
                $localAddresses[] = $calculator->cidr2network(
46
                    $interface->ipaddr,
47
                    intval($interface->subnet)
48
                ) . '/' . $interface->subnet;
49
            } else {
50
                $cidr             = $calculator->netmask2cidr($interface->subnet);
51
                $localAddresses[] = $calculator->cidr2network($interface->ipaddr, $cidr) . '/' . $cidr;
52
            }
53
        }
54
55
        $defaultRules             = FirewallRules::getDefaultRules();
56
        $networksTable            = [];
57
        $networkFilters           = NetworkFilters::find();
58
        $networkFiltersStoredInDB = ($networkFilters->count() > 0);
59
        foreach ($networkFilters as $filter) {
60
            $networksTable[$filter->id]['id']          = $filter->id;
61
            $networksTable[$filter->id]['description'] = $filter->description;
62
63
            $permitParts = explode('/', $filter->permit);
64
65
            if (!str_contains($permitParts[1], '.')) {
66
                $networksTable[$filter->id]['network'] = $calculator->cidr2network(
67
                    $permitParts[0],
68
                    intval($permitParts[1])
69
                ) . '/' . $permitParts[1];
70
            } else {
71
                $cidr                                  = $calculator->netmask2cidr($permitParts[1]);
72
                $networksTable[$filter->id]['network'] = $calculator->cidr2network(
73
                    $permitParts[0],
74
                    $cidr
75
                ) . '/' . $cidr;
76
            }
77
            $networksTable[$filter->id]['permanent'] = false;
78
79
80
            // Fill the default walues
81
            foreach ($defaultRules as $key => $value) {
82
                $networksTable[$filter->id]['category'][$key] = [
83
                    'name'   => empty($value['shortName']) ? $key : $value['shortName'],
84
                    'action' => $value['action'],
85
                ];
86
            }
87
88
            // Fill previous saved values
89
            $firewallRules = $filter->FirewallRules;
90
            foreach ($firewallRules as $rule) {
91
                $networksTable[$filter->id]['category'][$rule->category]['action'] = $rule->action;
92
                if (! array_key_exists('name', $networksTable[$filter->id]['category'][$rule->category])) {
93
                    $networksTable[$filter->id]['category'][$rule->category]['name'] = $rule->category;
94
                }
95
            }
96
        }
97
98
        // Add default filters
99
        foreach ($localAddresses as $localAddress) {
100
            $existsPersistentRecord = false;
101
            foreach ($networksTable as $key => $value) {
102
                if ($value['network'] === $localAddress) {
103
                    $networksTable[$key]['permanent'] = true;
104
                    $existsPersistentRecord           = true;
105
                    break;
106
                }
107
            }
108
            if (! $existsPersistentRecord) {
109
                foreach ($defaultRules as $key => $value) {
110
                    $networksTableNewRecord['category'][$key] = [
111
                        'name'   => $key,
112
                        'action' => $networkFiltersStoredInDB ? 'block' : $value['action'],
113
                    ];
114
                }
115
                $networksTableNewRecord['id']        = '';
116
                $networksTableNewRecord['permanent'] = true;
117
                $networksTableNewRecord['network']   = $localAddress;
118
                if ($localAddress === '0.0.0.0/0') {
119
                    $networksTableNewRecord['description'] = $this->translation->_('fw_AllNetworksRule');
120
                } else {
121
                    $networksTableNewRecord['description'] = $this->translation->_('fw_LocalNetworksRule');
122
                }
123
                $networksTable[] = $networksTableNewRecord;
124
            }
125
        }
126
127
        usort($networksTable, [__CLASS__, 'sortArrayByNetwork']);
128
129
        $this->view->rulesTable         = $networksTable;
130
        $this->view->PBXFirewallEnabled = PbxSettings::getValueByKey(PbxSettings::PBX_FIREWALL_ENABLED);
131
    }
132
133
134
    /**
135
     * Prepares forms to edit firewall rules
136
     *
137
     * @param string $networkId
138
     */
139
    public function modifyAction(string $networkId = ''): void
140
    {
141
        $networkFilter = NetworkFilters::findFirstById($networkId);
142
        $firewallRules = FirewallRules::getDefaultRules();
143
        $data          = $this->request->getPost();
144
        if ($networkFilter === null) {
145
            $networkFilter         = new NetworkFilters();
146
            $networkFilter->permit = empty($data['permit']) ? '0.0.0.0/0' : $data['permit'];
147
        } else {
148
            // Fill previous saved values
149
            foreach ($networkFilter->FirewallRules as $rule) {
150
                $firewallRules[$rule->category]['action'] = $rule->action;
151
            }
152
        }
153
        $permitParts = explode('/', $networkFilter->permit ?? '0.0.0.0/0');
154
155
        $this->view->form          = new FirewallEditForm(
156
            $networkFilter,
157
            ['network' => $permitParts[0], 'subnet' => $permitParts[1]]
158
        );
159
        $this->view->firewallRules = $firewallRules;
160
        $this->view->represent     = $networkFilter->getRepresent();
161
    }
162
163
164
    /**
165
     * Save request from the form
166
     */
167
    public function saveAction(): void
168
    {
169
        if (! $this->request->isPost()) {
170
            return;
171
        }
172
173
        $this->db->begin();
174
        $data      = $this->request->getPost();
175
        $networkId = $this->request->getPost('id');
176
        // Update network filters Network Filter
177
        $filterRecordId = $this->updateNetworkFilters($networkId, $data);
178
        if (empty($filterRecordId)) {
179
            $this->view->success = false;
180
            $this->db->rollback();
181
182
            return;
183
        }
184
185
        // If it was new entity we will reload page with new ID
186
        if (empty($data['id'])) {
187
            $this->view->reload = "firewall/modify/$filterRecordId";
188
        }
189
190
        // Update firewall rules Firewall
191
        $data['id'] = $filterRecordId;
192
        if (! $this->updateFirewallRules($data)) {
193
            $this->view->success = false;
194
            $this->db->rollback();
195
196
            return;
197
        }
198
199
        $this->flash->success($this->translation->_('ms_SuccessfulSaved'));
200
        $this->view->success = true;
201
        $this->db->commit();
202
    }
203
204
    /**
205
     * Fills Network Filter record
206
     *
207
     * @param string $networkId
208
     * @param array  $data POST parameters array
209
     *
210
     * @return string update result
211
     */
212
    private function updateNetworkFilters(string $networkId, array $data): string
213
    {
214
        $filterRecord = NetworkFilters::findFirstById($networkId);
215
        if ($filterRecord === null) {
216
            $filterRecord = new NetworkFilters();
217
        }
218
219
        $calculator = new Cidr();
220
        // Fills Network Filter record
221
        foreach ($filterRecord as $name => $value) {
222
            switch ($name) {
223
                case 'permit':
224
                    $filterRecord->$name = $calculator->cidr2network(
225
                        $data['network'],
226
                        intval($data['subnet'])
227
                    ) . '/' . $data['subnet'];
228
                    break;
229
                case 'deny':
230
                    $filterRecord->$name = '0.0.0.0/0';
231
                    break;
232
                case 'local_network':
233
                case 'newer_block_ip':
234
                    if (array_key_exists($name, $data) && $data[$name] === 'on') {
235
                        $filterRecord->$name = 1;
236
                    } else {
237
                        $filterRecord->$name = 0;
238
                    }
239
                    break;
240
                default:
241
                    if (array_key_exists($name, $data)) {
242
                        $filterRecord->$name = $data[$name];
243
                    }
244
            }
245
        }
246
247
        if ($filterRecord->save() === false) {
248
            $errors = $filterRecord->getMessages();
249
            $this->flash->error(implode('<br>', $errors));
250
251
            return '';
252
        }
253
254
        return $filterRecord->toArray()['id'];
255
    }
256
257
    /**
258
     * Updates firewall rules
259
     *
260
     * @param array $data POST parameters array
261
     *
262
     * @return bool update result
263
     */
264
    private function updateFirewallRules(array $data): bool
265
    {
266
        // Get default rules
267
        $defaultRules      = FirewallRules::getDefaultRules();
268
        $countDefaultRules = 0;
269
        foreach ($defaultRules as $key => $value) {
270
            foreach ($value['rules'] as $rule) {
271
                $countDefaultRules++;
272
            }
273
        }
274
275
        // Delete outdated records
276
        $parameters        = [
277
            'conditions' => 'networkfilterid=:networkfilterid:',
278
            'bind'       => [
279
                'networkfilterid' => $data['id'],
280
            ],
281
        ];
282
        $firewallRules     = FirewallRules::find($parameters);
283
        $currentRulesCount = $firewallRules->count();
284
285
        $needUpdateFirewallRules = false;
286
        while ($countDefaultRules < $currentRulesCount) {
287
            $firewallRules->next();
288
            if ($firewallRules->current()->delete() === false) {
289
                $errors = $firewallRules->getMessages();
290
                $this->flash->error(implode('<br>', $errors));
291
                $this->view->success = false;
292
293
                return false;
294
            }
295
            $currentRulesCount--;
296
            $needUpdateFirewallRules = true;
297
        }
298
299
        if ($needUpdateFirewallRules) {
300
            $firewallRules = FirewallRules::find($parameters);
301
        }
302
        $rowId = 0;
303
        foreach ($defaultRules as $key => $value) {
304
            foreach ($value['rules'] as $rule) {
305
                if ($firewallRules->offsetExists($rowId)) {
306
                    $newRule = $firewallRules->offsetGet($rowId);
307
                } else {
308
                    $newRule = new FirewallRules();
309
                }
310
                $newRule->networkfilterid = $data['id'];
311
                $newRule->protocol        = $rule['protocol'];
312
                $newRule->portfrom        = $rule['portfrom'];
313
                $newRule->portto          = $rule['portto'];
314
                $newRule->category        = $key;
315
                $newRule->portFromKey     = $rule['portFromKey'];
316
                $newRule->portToKey       = $rule['portToKey'];
317
318
                if (array_key_exists('rule_' . $key, $data) && $data['rule_' . $key]) {
319
                    $newRule->action = $data['rule_' . $key] === 'on' ? 'allow' : 'block';
320
                } else {
321
                    $newRule->action = 'block';
322
                }
323
                $newRule->description = "$newRule->action connection from network: {$data['network']} / {$data['subnet']}";
324
325
                if ($newRule->save() === false) {
326
                    $errors = $newRule->getMessages();
327
                    $this->flash->error(implode('<br>', $errors));
328
                    $this->view->success = false;
329
330
                    return false;
331
                }
332
                $rowId++;
333
            }
334
        }
335
336
        return true;
337
    }
338
339
    /**
340
     * Deletes NetworkFilters record
341
     *
342
     * @param string $networkId
343
     */
344
    public function deleteAction(string $networkId = ''): void
345
    {
346
        $this->db->begin();
347
        $filterRecord = NetworkFilters::findFirstById($networkId);
348
349
        $errors = null;
350
        if ($filterRecord !== null && ! $filterRecord->delete()) {
351
            $errors = $filterRecord->getMessages();
352
        }
353
354
        if ($errors) {
355
            $this->flash->warning(implode('<br>', $errors));
356
            $this->db->rollback();
357
        } else {
358
            $this->db->commit();
359
        }
360
361
        $this->forward('firewall/index');
362
    }
363
364
    /**
365
     * Enables Fail2Ban and Firewall
366
     */
367
    public function enableAction(): void
368
    {
369
        $fail2BanEnabled = PbxSettings::findFirstByKey(PbxSettings::PBX_FAIL2BAN_ENABLED);
370
        if ($fail2BanEnabled === null) {
371
            $fail2BanEnabled      = new PbxSettings();
372
            $fail2BanEnabled->key = PbxSettings::PBX_FAIL2BAN_ENABLED;
373
        }
374
        $fail2BanEnabled->value = '1';
375
        if ($fail2BanEnabled->save() === false) {
376
            $errors = $fail2BanEnabled->getMessages();
377
            $this->flash->warning(implode('<br>', $errors));
378
            $this->view->success = false;
379
380
            return;
381
        }
382
383
        $firewallEnabled = PbxSettings::findFirstByKey(PbxSettings::PBX_FIREWALL_ENABLED);
384
        if ($firewallEnabled === null) {
385
            $firewallEnabled      = new PbxSettings();
386
            $firewallEnabled->key = PbxSettings::PBX_FAIL2BAN_ENABLED;
387
        }
388
        $firewallEnabled->value = '1';
389
        if ($firewallEnabled->save() === false) {
390
            $errors = $firewallEnabled->getMessages();
391
            $this->flash->warning(implode('<br>', $errors));
392
            $this->view->success = false;
393
394
            return;
395
        }
396
        $this->view->success = true;
397
    }
398
399
    /**
400
     * Disables Fail2Ban and Firewall
401
     */
402
    public function disableAction(): void
403
    {
404
        $fail2BanEnabled = PbxSettings::findFirstByKey(PbxSettings::PBX_FAIL2BAN_ENABLED);
405
        if ($fail2BanEnabled === null) {
406
            $fail2BanEnabled      = new PbxSettings();
407
            $fail2BanEnabled->key = PbxSettings::PBX_FAIL2BAN_ENABLED;
408
        }
409
        $fail2BanEnabled->value = '0';
410
        if ($fail2BanEnabled->save() === false) {
411
            $errors = $fail2BanEnabled->getMessages();
412
            $this->flash->warning(implode('<br>', $errors));
413
            $this->view->success = false;
414
415
            return;
416
        }
417
418
        $firewallEnabled = PbxSettings::findFirstByKey(PbxSettings::PBX_FIREWALL_ENABLED);
419
        if ($firewallEnabled === null) {
420
            $firewallEnabled      = new PbxSettings();
421
            $firewallEnabled->key = PbxSettings::PBX_FAIL2BAN_ENABLED;
422
        }
423
        $firewallEnabled->value = '0';
424
        if ($firewallEnabled->save() === false) {
425
            $errors = $firewallEnabled->getMessages();
426
            $this->flash->warning(implode('<br>', $errors));
427
            $this->view->success = false;
428
429
            return;
430
        }
431
        $this->view->success = true;
432
    }
433
434
    /**
435
     * Compare two network entries for sorting
436
     *
437
     * @param array $a First network entry
438
     * @param array $b Second network entry
439
     * @return int Returns -1 if $a should be placed before $b,
440
     *             1 if $a should be placed after $b,
441
     *             0 if they are considered equal
442
     */
443
    private function sortArrayByNetwork(array $a, array $b): int
444
    {
445
        // If second entry is permanent and first is not 0.0.0.0/0
446
        if ($b['permanent'] && $a['network'] !== '0.0.0.0/0') {
447
            return 1; // Move $a after $b
448
        }
449
450
        // If second entry is 0.0.0.0/0
451
        if ($b['network'] === '0.0.0.0/0') {
452
            return 1; // Move $a after $b
453
        }
454
455
        return -1; // In all other cases, move $a before $b
456
    }
457
}
458