Completed
Pull Request — master (#18)
by Tomas Norre
03:41
created

RestrictionService::isClientRestricted()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.5021

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 6
cts 11
cp 0.5455
rs 9.9
c 0
b 0
f 0
cc 4
nc 3
nop 0
crap 5.5021
1
<?php
2
namespace Aoe\FeloginBruteforceProtection\Domain\Service;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2019 AOE GmbH <[email protected]>
8
 *
9
 *  All rights reserved
10
 *
11
 *  This script is part of the TYPO3 project. The TYPO3 project is
12
 *  free software; you can redistribute it and/or modify
13
 *  it under the terms of the GNU General Public License as published by
14
 *  the Free Software Foundation; either version 3 of the License, or
15
 *  (at your option) any later version.
16
 *
17
 *  The GNU General Public License can be found at
18
 *  http://www.gnu.org/copyleft/gpl.html.
19
 *
20
 *  This script is distributed in the hope that it will be useful,
21
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23
 *  GNU General Public License for more details.
24
 *
25
 *  This copyright notice MUST APPEAR in all copies of the script!
26
 ***************************************************************/
27
28
use Aoe\FeloginBruteforceProtection\Domain\Repository\EntryRepository;
29
use Aoe\FeloginBruteforceProtection\Service\Logger\LoggerInterface;
30
use Aoe\FeloginBruteforceProtection\System\Configuration;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
32
use Aoe\FeloginBruteforceProtection\Domain\Model\Entry;
33
use Aoe\FeloginBruteforceProtection\Service\Logger\Logger;
34
use Aoe\FeloginBruteforceProtection\Service\FeLoginBruteForceApi\FeLoginBruteForceApi;
35
use TYPO3\CMS\Extbase\Object\ObjectManager;
36
use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;
37
38
/**
39
 *
40
 * @package Aoe\\FeloginBruteforceProtection\\Domain\\Service
41
 *
42
 * @author Kevin Schu <[email protected]>
43
 * @author Timo Fuchs <[email protected]>
44
 * @author Andre Wuttig <[email protected]>
45
 *
46
 */
47
class RestrictionService
48
{
49
    /**
50
     * @var boolean
51
     */
52
    protected static $preventFailureCount = false;
53
54
    /**
55
     * @var RestrictionIdentifierInterface
56
     */
57
    protected $restrictionIdentifier;
58
59
    /**
60
     * @var string
61
     */
62
    protected $clientIdentifier;
63
64
    /**
65
     * @var \Aoe\FeloginBruteforceProtection\System\Configuration
66
     */
67
    protected $configuration;
68
69
    /**
70
     * @var \Aoe\FeloginBruteforceProtection\Domain\Repository\EntryRepository
71
     */
72
    protected $entryRepository;
73
74
    /**
75
     * @var \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager
76
     */
77
    protected $persistenceManager;
78
79
    /**
80
     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
81
     */
82
    protected $objectManager;
83
84
    /**
85
     * @var Entry
86
     */
87
    protected $entry;
88
89
    /**
90
     * @var boolean
91
     */
92
    protected $clientRestricted;
93
94
    /**
95
     * @var Logger
96
     */
97
    protected $logger;
98
99
    /**
100
     * @var FeLoginBruteForceApi
101
     */
102
    protected $feLoginBruteForceApi;
103
104
    /**
105
     * @param RestrictionIdentifierInterface $restrictionIdentifier
106
     */
107 26
    public function __construct(RestrictionIdentifierInterface $restrictionIdentifier)
108
    {
109 26
        $this->objectManager = GeneralUtility::makeInstance(ObjectManager::class);
110 26
        $this->restrictionIdentifier = $restrictionIdentifier;
111
112 26
        $this->configuration = $this->objectManager->get(Configuration::class);
113 26
        $this->persistenceManager = $this->objectManager->get(PersistenceManager::class);
114 26
        $this->entryRepository = $this->objectManager->get(EntryRepository::class);
115
116
117 26
    }
118
119
    /**
120
     * @param boolean $preventFailureCount
121
     * @return void
122
     */
123
    public static function setPreventFailureCount($preventFailureCount)
124
    {
125
        self::$preventFailureCount = $preventFailureCount;
126
    }
127
128
    /**
129
     * @return boolean
130
     */
131 16
    public function isClientRestricted()
132
    {
133 16
        if (false === isset($this->clientRestricted)) {
134 16
            if ($this->hasEntry() && $this->isRestricted($this->getEntry())) {
0 ignored issues
show
Bug introduced by
It seems like $this->getEntry() can be null; however, isRestricted() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
135 4
                $this->clientRestricted = true;
136
            } else {
137 12
                $this->clientRestricted = false;
138
            }
139
        }
140 16
        return $this->clientRestricted;
141
    }
142
143
    /**
144
     * @return void
145
     */
146 10
    public function removeEntry()
147
    {
148 10
        if ($this->hasEntry()) {
149 10
            $this->entryRepository->remove($this->entry);
150 10
            $this->persistenceManager->persistAll();
151
152 10
            $this->log('Bruteforce Counter removed', LoggerInterface::SEVERITY_INFO);
153
        }
154 10
        $this->clientRestricted = false;
155 10
        unset($this->entry);
156 10
    }
157
158
    /**
159
     * @return void
160
     */
161
    public function checkAndHandleRestriction()
162
    {
163
        if (self::$preventFailureCount) {
164
            return;
165
        }
166
167
        $identifierValue = $this->restrictionIdentifier->getIdentifierValue();
168
        if (empty($identifierValue)) {
169
            return;
170
        }
171
172
        if (false === $this->hasEntry()) {
173
            $this->createEntry();
174
        }
175
176
        if ($this->hasMaximumNumberOfFailuresReached($this->getEntry())) {
0 ignored issues
show
Bug introduced by
It seems like $this->getEntry() can be null; however, hasMaximumNumberOfFailuresReached() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
177
            return;
178
        }
179
180
        $this->entry->increaseFailures();
181
        $this->saveEntry();
182
183
        $this->restrictionLog();
184
    }
185
186
    /**
187
     * @return void
188
     */
189
    protected function restrictionLog()
190
    {
191
        if ($this->getFeLoginBruteForceApi()->shouldCountWithinThisRequest()) {
192
            if ($this->isClientRestricted()) {
193
                $this->log('Bruteforce Protection Locked', LoggerInterface::SEVERITY_WARNING);
194
            } else {
195
                $this->log('Bruteforce Counter increased', LoggerInterface::SEVERITY_NOTICE);
196
            }
197
        } else {
198
            $this->log(
199
                'Bruteforce Counter would increase, but is prohibited by API',
200
                LoggerInterface::SEVERITY_NOTICE
201
            );
202
        }
203
    }
204
205
    /**
206
     * @param $message
207
     * @param $severity
208
     */
209 10
    private function log($message, $severity)
210
    {
211 10
        $failureCount = 0;
212 10
        if ($this->hasEntry()) {
213 10
            $failureCount = $this->getEntry()->getFailures();
214
        }
215 10
        if ($this->isClientRestricted()) {
216
            $restricted = 'Yes';
217
        } else {
218 10
            $restricted = 'No';
219
        }
220
        $additionalData = array(
221 10
            'FAILURE_COUNT' => $failureCount,
222 10
            'RESTRICTED' => $restricted,
223 10
            'REMOTE_ADDR' => GeneralUtility::getIndpEnv('REMOTE_ADDR'),
224 10
            'REQUEST_URI' => GeneralUtility::getIndpEnv('REQUEST_URI'),
225 10
            'HTTP_USER_AGENT' => GeneralUtility::getIndpEnv('HTTP_USER_AGENT')
226
        );
227
228 10
        $this->getLogger()->log($message, $severity, $additionalData, 'felogin_bruteforce_protection');
229 10
    }
230
231
    /**
232
     * @return Logger
233
     */
234 10
    private function getLogger()
235
    {
236 10
        if (!isset($this->logger)) {
237
            $this->logger = new Logger();
238
        }
239 10
        return $this->logger;
240
    }
241
242
    /**
243
     * @return void
244
     */
245
    private function createEntry()
246
    {
247
        /** @var $entry Entry */
248
        $this->entry = $this->objectManager->get('Aoe\FeloginBruteforceProtection\Domain\Model\Entry');
249
        $this->entry->setFailures(0);
250
        $this->entry->setCrdate(time());
251
        $this->entry->setTstamp(time());
252
        $this->entry->setIdentifier($this->getClientIdentifier());
253
        $this->entryRepository->add($this->entry);
254
        $this->persistenceManager->persistAll();
255
        $this->clientRestricted = false;
256
    }
257
258
    /**
259
     * @return void
260
     */
261
    private function saveEntry()
262
    {
263
        if ($this->entry->getFailures() > 0) {
264
            $this->entry->setTstamp(time());
265
        }
266
        $this->entryRepository->add($this->entry);
267
        $this->persistenceManager->persistAll();
268
        if ($this->hasMaximumNumberOfFailuresReached($this->entry)) {
269
            $this->clientRestricted = true;
270
        }
271
    }
272
273
    /**
274
     * @param Entry $entry
275
     * @return boolean
276
     */
277 16
    private function isRestricted(Entry $entry)
278
    {
279 16
        if ($this->hasMaximumNumberOfFailuresReached($entry)) {
280 8
            if (false === $this->isRestrictionTimeReached($entry)) {
281 4
                return true;
282
            }
283
        }
284 12
        return false;
285
    }
286
287
    /**
288
     * @return boolean
289
     */
290 16
    public function hasEntry()
291
    {
292 16
        return ($this->getEntry() instanceof Entry);
293
    }
294
295
    /**
296
     * @return Entry|null
297
     */
298 16
    public function getEntry()
299
    {
300 16
        if (false === isset($this->entry)) {
301 16
            $entry = $this->entryRepository->findByIdentifier($this->getClientIdentifier());
302 16
            if ($entry instanceof Entry) {
303 16
                $this->entry = $entry;
304 16
                if ($this->isOutdated($entry)) {
305 10
                    $this->removeEntry();
306
                }
307
            }
308
        }
309 16
        return $this->entry;
310
    }
311
312
    /**
313
     * @param Entry $entry
314
     * @return boolean
315
     */
316 16
    private function isOutdated(Entry $entry)
317
    {
318
        return (
319 16
            ($this->hasMaximumNumberOfFailuresReached($entry) && $this->isRestrictionTimeReached($entry)) ||
320 16
            (false === $this->hasMaximumNumberOfFailuresReached($entry) && $this->isResetTimeOver($entry))
321
        );
322
    }
323
324
    /**
325
     * @param Entry $entry
326
     * @return boolean
327
     */
328 8
    private function isResetTimeOver(Entry $entry)
329
    {
330 8
        return ($entry->getCrdate() < time() - $this->configuration->getResetTime());
331
    }
332
333
    /**
334
     * @param Entry $entry
335
     * @return boolean
336
     */
337 16
    private function hasMaximumNumberOfFailuresReached(Entry $entry)
338
    {
339 16
        return ($entry->getFailures() >= $this->configuration->getMaximumNumberOfFailures());
340
    }
341
342
    /**
343
     * @param Entry $entry
344
     * @return boolean
345
     */
346 8
    private function isRestrictionTimeReached(Entry $entry)
347
    {
348 8
        return ($entry->getTstamp() < time() - $this->configuration->getRestrictionTime());
349
    }
350
351
    /**
352
     * Returns the client identifier based on the clients IP address.
353
     *
354
     * @return string
355
     */
356 16
    private function getClientIdentifier()
357
    {
358 16
        if (false === isset($this->clientIdentifier)) {
359 16
            $this->clientIdentifier = md5(
360 16
                $this->restrictionIdentifier->getIdentifierValue() . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
361
            );
362
        }
363 16
        return $this->clientIdentifier;
364
    }
365
366
    /**
367
     * @return FeLoginBruteForceApi
368
     */
369
    protected function getFeLoginBruteForceApi()
370
    {
371
        if (!isset($this->feLoginBruteForceApi)) {
372
            $this->feLoginBruteForceApi = $this->objectManager->get(
373
                'Aoe\FeloginBruteforceProtection\Service\FeLoginBruteForceApi\FeLoginBruteForceApi'
374
            );
375
        }
376
        return $this->feLoginBruteForceApi;
377
    }
378
}
379