Passed
Push — 2.x ( ed3db8...f88ed3 )
by Terry
01:57
created

RuleTrait   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 218
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 85
c 3
b 0
f 0
dl 0
loc 218
rs 10
wmc 22

4 Methods

Rating   Name   Duplication   Size   Complexity  
A prepareMessengerBody() 0 22 2
B determineAttemptsPermanentDeny() 0 43 7
A determineAttemptsTemporaryDeny() 0 24 4
B IsRuleExist() 0 71 9
1
<?php
2
/*
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace Shieldon\Firewall\Kernel;
14
15
use Shieldon\Firewall\Kernel;
16
use function Shieldon\Firewall\__;
17
use function Shieldon\Firewall\get_cpu_usage;
18
use function Shieldon\Firewall\get_memory_usage;
19
use function file_exists;
20
use function file_put_contents;
21
use function filter_var;
22
use function is_writable;
23
use function time;
24
25
/*
26
 * @since 1.0.0
27
 */
28
trait RuleTrait
29
{
30
    /**
31
     * The events.
32
     *
33
     * @var array
34
     */
35
    protected $event = [
36
37
        // Update rule table when this value true.
38
        'update_rule_table' => false,
39
40
        // Send notifications when this value true.
41
        'trigger_messengers' => false,
42
    ];
43
44
    /**
45
     * Look up the rule table.
46
     *
47
     * If a specific IP address doesn't exist, return false. 
48
     * Otherwise, return true.
49
     *
50
     * @return bool
51
     */
52
    private function IsRuleExist()
53
    {
54
        $ipRule = $this->driver->get($this->ip, 'rule');
55
56
        if (empty($ipRule)) {
57
            return false;
58
        }
59
60
        $ruleType = (int) $ipRule['type'];
61
62
        // Apply the status code.
63
        $this->result = $ruleType;
0 ignored issues
show
Bug Best Practice introduced by
The property result does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
64
65
        if ($ruleType === kernel::ACTION_ALLOW) {
66
            return true;
67
        }
68
69
        // Current visitor has been blocked. If he still attempts accessing the site, 
70
        // then we can drop him into the permanent block list.
71
        $attempts = $ipRule['attempts'] ?? 0;
72
        $attempts = (int) $attempts;
73
        $now = time();
74
        $logData = [];
75
        $handleType = 0;
76
77
        $logData['log_ip']     = $ipRule['log_ip'];
78
        $logData['ip_resolve'] = $ipRule['ip_resolve'];
79
        $logData['time']       = $now;
80
        $logData['type']       = $ipRule['type'];
81
        $logData['reason']     = $ipRule['reason'];
82
        $logData['attempts']   = $attempts;
83
84
        // @since 0.2.0
85
        $attemptPeriod = $this->properties['record_attempt_detection_period'];
86
        $attemptReset  = $this->properties['reset_attempt_counter'];
87
88
        $lastTimeDiff = $now - $ipRule['time'];
89
90
        if ($lastTimeDiff <= $attemptPeriod) {
91
            $logData['attempts'] = ++$attempts;
92
        }
93
94
        if ($lastTimeDiff > $attemptReset) {
95
            $logData['attempts'] = 0;
96
        }
97
98
        if ($ruleType === kernel::ACTION_TEMPORARILY_DENY) {
99
            $ratd = $this->determineAttemptsTemporaryDeny($logData, $handleType, $attempts);
100
            $logData = $ratd['log_data'];
101
            $handleType = $ratd['handle_type'];
102
        }
103
104
        if ($ruleType === kernel::ACTION_DENY) {
105
            $rapd = $this->determineAttemptsPermanentDeny($logData, $handleType, $attempts);
106
            $logData = $rapd['log_data'];
107
            $handleType = $rapd['handle_type'];
108
        }
109
110
        // We only update data when `deny_attempt_enable` is enable.
111
        // Because we want to get the last visited time and attempt counter.
112
        // Otherwise, we don't update it everytime to avoid wasting CPU resource.
113
        if ($this->event['update_rule_table']) {
114
            $this->driver->save($this->ip, $logData, 'rule');
115
        }
116
117
        // Notify this event to messenger.
118
        if ($this->event['trigger_messengers']) {
119
            $this->prepareMessengerBody($logData, $handleType);
120
        }
121
122
        return true;
123
    }
124
125
    /**
126
     * Record the attempts when the user is temporarily denied by rule table.
127
     *
128
     * @param array $logData
129
     * @param int   $handleType
130
     * @param int   $attempts
131
     * 
132
     * @return array
133
     */
134
    private function determineAttemptsTemporaryDeny(array $logData, int $handleType, int $attempts): array
135
    {
136
        if ($this->properties['deny_attempt_enable']['data_circle']) {
137
            $this->event['update_rule_table'] = true;
138
139
            $buffer = $this->properties['deny_attempt_buffer']['data_circle'];
140
141
            if ($attempts >= $buffer) {
142
143
                if ($this->properties['deny_attempt_notify']['data_circle']) {
144
                    $this->event['trigger_messengers'] = true;
145
                }
146
147
                $logData['type'] = kernel::ACTION_DENY;
148
149
                // Reset this value for next checking process - iptables.
150
                $logData['attempts'] = 0;
151
                $handleType = 1;
152
            }
153
        }
154
155
        return [
156
            'log_data' => $logData,
157
            'handle_type' => $handleType,
158
        ];
159
    }
160
161
    /**
162
     * Record the attempts when the user is permanently denied by rule table.
163
     *
164
     * @param array $logData
165
     * @param int   $handleType
166
     * @param int   $attempts
167
     * 
168
     * @return array
169
     */
170
    private function determineAttemptsPermanentDeny(array $logData, int $handleType, int $attempts): array
171
    {
172
        if ($this->properties['deny_attempt_enable']['system_firewall']) {
173
            $this->event['update_rule_table'] = true;
174
175
            // For the requests that are already banned, but they are still attempting access, that means 
176
            // that they are programmably accessing your website. Consider put them in the system-layer fireall
177
            // such as IPTABLE.
178
            $bufferIptable = $this->properties['deny_attempt_buffer']['system_firewall'];
179
180
            if ($attempts >= $bufferIptable) {
181
182
                if ($this->properties['deny_attempt_notify']['system_firewall']) {
183
                    $this->event['trigger_messengers'] = true;
184
                }
185
186
                $folder = rtrim($this->properties['iptables_watching_folder'], '/');
187
188
                if (file_exists($folder) && is_writable($folder)) {
189
                    $filePath = $folder . '/iptables_queue.log';
190
191
                    // command, ipv4/6, ip, subnet, port, protocol, action
192
                    // add,4,127.0.0.1,null,all,all,drop  (example)
193
                    // add,4,127.0.0.1,null,80,tcp,drop   (example)
194
                    $command = 'add,4,' . $this->ip . ',null,all,all,deny';
195
196
                    if (filter_var($this->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
197
                        $command = 'add,6,' . $this->ip . ',null,all,allow';
198
                    }
199
200
                    // Add this IP address to itables_queue.log
201
                    // Use `bin/iptables.sh` for adding it into IPTABLES. See document for more information. 
202
                    file_put_contents($filePath, $command . "\n", FILE_APPEND | LOCK_EX);
203
204
                    $logData['attempts'] = 0;
205
                    $handleType = 2;
206
                }
207
            }
208
        }
209
210
        return [
211
            'log_data' => $logData,
212
            'handle_type' => $handleType,
213
        ];
214
    }
215
216
    /**
217
     * Prepare the message body for messenger modules to sent.
218
     *
219
     * @param array $logData
220
     * @param int   $handleType
221
     * 
222
     * @return void
223
     */
224
    private function prepareMessengerBody(array $logData, int $handleType): void
225
    {
226
        // The data strings that will be appended to message body.
227
        $prepareMessageData = [
228
            __('core', 'messenger_text_ip')       => $logData['log_ip'],
229
            __('core', 'messenger_text_rdns')     => $logData['ip_resolve'],
230
            __('core', 'messenger_text_reason')   => __('core', 'messenger_text_reason_code_' . $logData['reason']),
231
            __('core', 'messenger_text_handle')   => __('core', 'messenger_text_handle_type_' . $handleType),
232
            __('core', 'messenger_text_system')   => '',
233
            __('core', 'messenger_text_cpu')      => get_cpu_usage(),
234
            __('core', 'messenger_text_memory')   => get_memory_usage(),
235
            __('core', 'messenger_text_time')     => date('Y-m-d H:i:s', $logData['time']),
236
            __('core', 'messenger_text_timezone') => date_default_timezone_get(),
237
        ];
238
239
        $message = __('core', 'messenger_notification_subject', 'Notification for {0}', [$this->ip]) . "\n\n";
240
241
        foreach ($prepareMessageData as $key => $value) {
242
            $message .= $key . ': ' . $value . "\n";
243
        }
244
245
        $this->msgBody = $message;
0 ignored issues
show
Bug Best Practice introduced by
The property msgBody does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
246
    }
247
}
248