Completed
Pull Request — master (#18)
by Tomas Norre
13:28
created

RestrictionService::getEntry()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 0
cts 13
cp 0
rs 9.8333
c 0
b 0
f 0
cc 4
nc 4
nop 0
crap 20
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
    public function __construct(RestrictionIdentifierInterface $restrictionIdentifier)
108
    {
109
        $this->objectManager = GeneralUtility::makeInstance(ObjectManager::class);
110
        $this->restrictionIdentifier = $restrictionIdentifier;
111
112
        $this->configuration = $this->objectManager->get(Configuration::class);
113
        $this->persistenceManager = $this->objectManager->get(PersistenceManager::class);
114
        $this->entryRepository = $this->objectManager->get(EntryRepository::class);
115
116
117
    }
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
    public function isClientRestricted()
132
    {
133
        if (false === isset($this->clientRestricted)) {
134
            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
                $this->clientRestricted = true;
136
            } else {
137
                $this->clientRestricted = false;
138
            }
139
        }
140
        return $this->clientRestricted;
141
    }
142
143
    /**
144
     * @return void
145
     */
146
    public function removeEntry()
147
    {
148
        if ($this->hasEntry()) {
149
            $this->entryRepository->remove($this->entry);
150
            $this->persistenceManager->persistAll();
151
152
            $this->log('Bruteforce Counter removed', LoggerInterface::SEVERITY_INFO);
153
        }
154
        $this->clientRestricted = false;
155
        unset($this->entry);
156
    }
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
    private function log($message, $severity)
210
    {
211
        $failureCount = 0;
212
        if ($this->hasEntry()) {
213
            $failureCount = $this->getEntry()->getFailures();
214
        }
215
        if ($this->isClientRestricted()) {
216
            $restricted = 'Yes';
217
        } else {
218
            $restricted = 'No';
219
        }
220
        $additionalData = array(
221
            'FAILURE_COUNT' => $failureCount,
222
            'RESTRICTED' => $restricted,
223
            'REMOTE_ADDR' => GeneralUtility::getIndpEnv('REMOTE_ADDR'),
224
            'REQUEST_URI' => GeneralUtility::getIndpEnv('REQUEST_URI'),
225
            'HTTP_USER_AGENT' => GeneralUtility::getIndpEnv('HTTP_USER_AGENT')
226
        );
227
228
        $this->getLogger()->log($message, $severity, $additionalData, 'felogin_bruteforce_protection');
229
    }
230
231
    /**
232
     * @return Logger
233
     */
234
    private function getLogger()
235
    {
236
        if (!isset($this->logger)) {
237
            $this->logger = new Logger();
238
        }
239
        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
    private function isRestricted(Entry $entry)
278
    {
279
        if ($this->hasMaximumNumberOfFailuresReached($entry)) {
280
            if (false === $this->isRestrictionTimeReached($entry)) {
281
                return true;
282
            }
283
        }
284
        return false;
285
    }
286
287
    /**
288
     * @return boolean
289
     */
290
    public function hasEntry()
291
    {
292
        return ($this->getEntry() instanceof Entry);
293
    }
294
295
    /**
296
     * @return Entry|null
297
     */
298
    public function getEntry()
299
    {
300
        if (false === isset($this->entry)) {
301
            $entry = $this->entryRepository->findByIdentifier($this->getClientIdentifier());
302
            if ($entry instanceof Entry) {
303
                $this->entry = $entry;
304
                if ($this->isOutdated($entry)) {
305
                    $this->removeEntry();
306
                }
307
            }
308
        }
309
        return $this->entry;
310
    }
311
312
    /**
313
     * @param Entry $entry
314
     * @return boolean
315
     */
316
    private function isOutdated(Entry $entry)
317
    {
318
        return (
319
            ($this->hasMaximumNumberOfFailuresReached($entry) && $this->isRestrictionTimeReached($entry)) ||
320
            (false === $this->hasMaximumNumberOfFailuresReached($entry) && $this->isResetTimeOver($entry))
321
        );
322
    }
323
324
    /**
325
     * @param Entry $entry
326
     * @return boolean
327
     */
328
    private function isResetTimeOver(Entry $entry)
329
    {
330
        return ($entry->getCrdate() < time() - $this->configuration->getResetTime());
331
    }
332
333
    /**
334
     * @param Entry $entry
335
     * @return boolean
336
     */
337
    private function hasMaximumNumberOfFailuresReached(Entry $entry)
338
    {
339
        return ($entry->getFailures() >= $this->configuration->getMaximumNumberOfFailures());
340
    }
341
342
    /**
343
     * @param Entry $entry
344
     * @return boolean
345
     */
346
    private function isRestrictionTimeReached(Entry $entry)
347
    {
348
        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
    private function getClientIdentifier()
357
    {
358
        if (false === isset($this->clientIdentifier)) {
359
            $this->clientIdentifier = md5(
360
                $this->restrictionIdentifier->getIdentifierValue() . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
361
            );
362
        }
363
        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