Completed
Pull Request — master (#15)
by Tomas Norre
05:10 queued 03:28
created

RestrictionService::checkAndHandleRestriction()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 0
cts 14
cp 0
rs 9.2248
c 0
b 0
f 0
cc 5
nc 6
nop 0
crap 30
1
<?php
2
namespace Aoe\FeloginBruteforceProtection\Domain\Service;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2015 AOE GmbH, <[email protected]>
8
 *  (c) 2014 André Wuttig <[email protected]>, portrino GmbH
9
 *
10
 *  All rights reserved
11
 *
12
 *  This script is part of the TYPO3 project. The TYPO3 project is
13
 *  free software; you can redistribute it and/or modify
14
 *  it under the terms of the GNU General Public License as published by
15
 *  the Free Software Foundation; either version 3 of the License, or
16
 *  (at your option) any later version.
17
 *
18
 *  The GNU General Public License can be found at
19
 *  http://www.gnu.org/copyleft/gpl.html.
20
 *
21
 *  This script is distributed in the hope that it will be useful,
22
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
 *  GNU General Public License for more details.
25
 *
26
 *  This copyright notice MUST APPEAR in all copies of the script!
27
 ***************************************************************/
28
29
use Aoe\FeloginBruteforceProtection\Service\Logger\LoggerInterface;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
use Aoe\FeloginBruteforceProtection\Domain\Model\Entry;
32
use Aoe\FeloginBruteforceProtection\Service\Logger\Logger;
33
use Aoe\FeloginBruteforceProtection\Service\FeLoginBruteForceApi\FeLoginBruteForceApi;
34
35
/**
36
 *
37
 * @package Aoe\\FeloginBruteforceProtection\\Domain\\Service
38
 *
39
 * @author Kevin Schu <[email protected]>
40
 * @author Timo Fuchs <[email protected]>
41
 * @author Andre Wuttig <[email protected]>
42
 *
43
 */
44
class RestrictionService
45
{
46
    /**
47
     * @var boolean
48
     */
49
    protected static $preventFailureCount = false;
50
51
    /**
52
     * @var RestrictionIdentifierInterface
53
     */
54
    protected $restrictionIdentifier;
55
56
    /**
57
     * @var string
58
     */
59
    protected $clientIdentifier;
60
61
    /**
62
     * @var \Aoe\FeloginBruteforceProtection\System\Configuration
63
     * @inject
64
     */
65
    protected $configuration;
66
67
    /**
68
     * @var \Aoe\FeloginBruteforceProtection\Domain\Repository\EntryRepository
69
     * @inject
70
     */
71
    protected $entryRepository;
72
73
    /**
74
     * @var \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager
75
     * @inject
76
     */
77
    protected $persistenceManager;
78
79
    /**
80
     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
81
     * @inject
82
     */
83
    protected $objectManager;
84
85
    /**
86
     * @var Entry
87
     */
88
    protected $entry;
89
90
    /**
91
     * @var boolean
92
     */
93
    protected $clientRestricted;
94
95
    /**
96
     * @var Logger
97
     */
98
    protected $logger;
99
100
    /**
101
     * @var FeLoginBruteForceApi
102
     */
103
    protected $feLoginBruteForceApi;
104
105
    /**
106
     * @param RestrictionIdentifierInterface $restrictionIdentifier
107
     */
108 26
    public function __construct(RestrictionIdentifierInterface $restrictionIdentifier)
109
    {
110 26
        $this->restrictionIdentifier = $restrictionIdentifier;
111 26
    }
112
113
    /**
114
     * @param boolean $preventFailureCount
115
     * @return void
116
     */
117
    public static function setPreventFailureCount($preventFailureCount)
118
    {
119
        self::$preventFailureCount = $preventFailureCount;
120
    }
121
122
    /**
123
     * @return boolean
124
     */
125 16
    public function isClientRestricted()
126
    {
127 16
        if (false === isset($this->clientRestricted)) {
128 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...
129 4
                $this->clientRestricted = true;
130
            } else {
131 12
                $this->clientRestricted = false;
132
            }
133
        }
134 16
        return $this->clientRestricted;
135
    }
136
137
    /**
138
     * @return void
139
     */
140 10
    public function removeEntry()
141
    {
142 10
        if ($this->hasEntry()) {
143 10
            $this->entryRepository->remove($this->entry);
144 10
            $this->persistenceManager->persistAll();
145
146 10
            $this->log('Bruteforce Counter removed', LoggerInterface::SEVERITY_INFO);
147
        }
148 10
        $this->clientRestricted = false;
149 10
        unset($this->entry);
150 10
    }
151
152
    /**
153
     * @return void
154
     */
155
    public function checkAndHandleRestriction()
156
    {
157
        if (self::$preventFailureCount) {
158
            return;
159
        }
160
161
        $identifierValue = $this->restrictionIdentifier->getIdentifierValue();
162
        if (empty($identifierValue)) {
163
            return;
164
        }
165
166
        if (false === $this->hasEntry()) {
167
            $this->createEntry();
168
        }
169
170
        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...
171
            return;
172
        }
173
174
        $this->entry->increaseFailures();
175
        $this->saveEntry();
176
177
        $this->restrictionLog();
178
    }
179
180
    /**
181
     * @return void
182
     */
183
    protected function restrictionLog()
184
    {
185
        if ($this->getFeLoginBruteForceApi()->shouldCountWithinThisRequest()) {
186
            if ($this->isClientRestricted()) {
187
                $this->log('Bruteforce Protection Locked', LoggerInterface::SEVERITY_WARNING);
188
            } else {
189
                $this->log('Bruteforce Counter increased', LoggerInterface::SEVERITY_NOTICE);
190
            }
191
        } else {
192
            $this->log(
193
                'Bruteforce Counter would increase, but is prohibited by API',
194
                LoggerInterface::SEVERITY_NOTICE
195
            );
196
        }
197
    }
198
199
    /**
200
     * @param $message
201
     * @param $severity
202
     */
203 10
    private function log($message, $severity)
204
    {
205 10
        $failureCount = 0;
206 10
        if ($this->hasEntry()) {
207 10
            $failureCount = $this->getEntry()->getFailures();
208
        }
209 10
        if ($this->isClientRestricted()) {
210
            $restricted = 'Yes';
211
        } else {
212 10
            $restricted = 'No';
213
        }
214
        $additionalData = array(
215 10
            'FAILURE_COUNT' => $failureCount,
216 10
            'RESTRICTED' => $restricted,
217 10
            'REMOTE_ADDR' => GeneralUtility::getIndpEnv('REMOTE_ADDR'),
218 10
            'REQUEST_URI' => GeneralUtility::getIndpEnv('REQUEST_URI'),
219 10
            'HTTP_USER_AGENT' => GeneralUtility::getIndpEnv('HTTP_USER_AGENT')
220
        );
221
222 10
        $this->getLogger()->log($message, $severity, $additionalData, 'felogin_bruteforce_protection');
223 10
    }
224
225
    /**
226
     * @return Logger
227
     */
228 10
    private function getLogger()
229
    {
230 10
        if (!isset($this->logger)) {
231
            $this->logger = new Logger();
232
        }
233 10
        return $this->logger;
234
    }
235
236
    /**
237
     * @return void
238
     */
239
    private function createEntry()
240
    {
241
        /** @var $entry Entry */
242
        $this->entry = $this->objectManager->get('Aoe\FeloginBruteforceProtection\Domain\Model\Entry');
243
        $this->entry->setFailures(0);
244
        $this->entry->setCrdate(time());
245
        $this->entry->setTstamp(time());
246
        $this->entry->setIdentifier($this->getClientIdentifier());
247
        $this->entryRepository->add($this->entry);
248
        $this->persistenceManager->persistAll();
249
        $this->clientRestricted = false;
250
    }
251
252
    /**
253
     * @return void
254
     */
255
    private function saveEntry()
256
    {
257
        if ($this->entry->getFailures() > 0) {
258
            $this->entry->setTstamp(time());
259
        }
260
        $this->entryRepository->add($this->entry);
261
        $this->persistenceManager->persistAll();
262
        if ($this->hasMaximumNumberOfFailuresReached($this->entry)) {
263
            $this->clientRestricted = true;
264
        }
265
    }
266
267
    /**
268
     * @param Entry $entry
269
     * @return boolean
270
     */
271 16
    private function isRestricted(Entry $entry)
272
    {
273 16
        if ($this->hasMaximumNumberOfFailuresReached($entry)) {
274 8
            if (false === $this->isRestrictionTimeReached($entry)) {
275 4
                return true;
276
            }
277
        }
278 12
        return false;
279
    }
280
281
    /**
282
     * @return boolean
283
     */
284 16
    public function hasEntry()
285
    {
286 16
        return ($this->getEntry() instanceof Entry);
287
    }
288
289
    /**
290
     * @return Entry|null
291
     */
292 16
    public function getEntry()
293
    {
294 16
        if (false === isset($this->entry)) {
295 16
            $entry = $this->entryRepository->findOneByIdentifier($this->getClientIdentifier());
0 ignored issues
show
Bug introduced by
The method findOneByIdentifier() does not exist on Aoe\FeloginBruteforcePro...ository\EntryRepository. Did you maybe mean findByIdentifier()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
296 16
            if ($entry instanceof Entry) {
297 16
                $this->entry = $entry;
298 16
                if ($this->isOutdated($entry)) {
299 10
                    $this->removeEntry();
300
                }
301
            }
302
        }
303 16
        return $this->entry;
304
    }
305
306
    /**
307
     * @param Entry $entry
308
     * @return boolean
309
     */
310 16
    private function isOutdated(Entry $entry)
311
    {
312
        return (
313 16
            ($this->hasMaximumNumberOfFailuresReached($entry) && $this->isRestrictionTimeReached($entry)) ||
314 16
            (false === $this->hasMaximumNumberOfFailuresReached($entry) && $this->isResetTimeOver($entry))
315
        );
316
    }
317
318
    /**
319
     * @param Entry $entry
320
     * @return boolean
321
     */
322 8
    private function isResetTimeOver(Entry $entry)
323
    {
324 8
        return ($entry->getCrdate() < time() - $this->configuration->getResetTime());
325
    }
326
327
    /**
328
     * @param Entry $entry
329
     * @return boolean
330
     */
331 16
    private function hasMaximumNumberOfFailuresReached(Entry $entry)
332
    {
333 16
        return ($entry->getFailures() >= $this->configuration->getMaximumNumberOfFailures());
334
    }
335
336
    /**
337
     * @param Entry $entry
338
     * @return boolean
339
     */
340 8
    private function isRestrictionTimeReached(Entry $entry)
341
    {
342 8
        return ($entry->getTstamp() < time() - $this->configuration->getRestrictionTime());
343
    }
344
345
    /**
346
     * Returns the client identifier based on the clients IP address.
347
     *
348
     * @return string
349
     */
350 16
    private function getClientIdentifier()
351
    {
352 16
        if (false === isset($this->clientIdentifier)) {
353 16
            $this->clientIdentifier = md5(
354 16
                $this->restrictionIdentifier->getIdentifierValue() . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
355
            );
356
        }
357 16
        return $this->clientIdentifier;
358
    }
359
360
    /**
361
     * @return FeLoginBruteForceApi
362
     */
363
    protected function getFeLoginBruteForceApi()
364
    {
365
        if (!isset($this->feLoginBruteForceApi)) {
366
            $this->feLoginBruteForceApi = $this->objectManager->get(
367
                'Aoe\FeloginBruteforceProtection\Service\FeLoginBruteForceApi\FeLoginBruteForceApi'
368
            );
369
        }
370
        return $this->feLoginBruteForceApi;
371
    }
372
}
373