Completed
Push — master ( df4949...df4068 )
by Joschi
03:25
created

HmacValidator   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 385
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 91.07%

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 9
dl 0
loc 385
ccs 102
cts 112
cp 0.9107
rs 9.0399
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setMethodVector() 0 11 2
A validateRequestMethod() 0 12 3
A setSubmissionTimes() 0 15 3
A validate() 0 11 3
A armor() 0 22 2
A validateHmac() 0 25 3
A validateRequestMethodVector() 0 16 4
B validateRequestTiming() 0 38 9
A probeTimedHmacAsInitialAndFollowup() 0 19 4
A probeTimedHmac() 0 17 3
B calculateHmac() 0 32 6

How to fix   Complexity   

Complex Class

Complex classes like HmacValidator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HmacValidator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * antibot
5
 *
6
 * @category   Jkphl
7
 * @package    Jkphl\Antibot
8
 * @subpackage Jkphl\Antibot\Ports\Validators
9
 * @author     Joschi Kuphal <[email protected]> / @jkphl
10
 * @copyright  Copyright © 2018 Joschi Kuphal <[email protected]> / @jkphl
11
 * @license    http://opensource.org/licenses/MIT The MIT License (MIT)
12
 */
13
14
/***********************************************************************************
15
 *  The MIT License (MIT)
16
 *
17
 *  Copyright © 2018 Joschi Kuphal <[email protected]>
18
 *
19
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of
20
 *  this software and associated documentation files (the "Software"), to deal in
21
 *  the Software without restriction, including without limitation the rights to
22
 *  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
23
 *  the Software, and to permit persons to whom the Software is furnished to do so,
24
 *  subject to the following conditions:
25
 *
26
 *  The above copyright notice and this permission notice shall be included in all
27
 *  copies or substantial portions of the Software.
28
 *
29
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
31
 *  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
32
 *  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
33
 *  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
34
 *  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35
 ***********************************************************************************/
36
37
namespace Jkphl\Antibot\Ports\Validators;
38
39
use Jkphl\Antibot\Domain\Antibot;
40
use Jkphl\Antibot\Domain\Exceptions\InvalidRequestMethodOrderException;
41
use Jkphl\Antibot\Domain\Exceptions\SkippedValidationException;
42
use Jkphl\Antibot\Infrastructure\Exceptions\HmacValidationException;
43
use Jkphl\Antibot\Infrastructure\Factory\HmacFactory;
44
use Jkphl\Antibot\Infrastructure\Model\AbstractValidator;
45
use Jkphl\Antibot\Infrastructure\Model\InputElement;
46
use Jkphl\Antibot\Ports\Exceptions\InvalidArgumentException;
47
use Psr\Http\Message\ServerRequestInterface;
48
49
/**
50
 * HMAC Validator
51
 *
52
 * @package    Jkphl\Antibot
53
 * @subpackage Jkphl\Antibot\Ports\Validators
54
 */
55
class HmacValidator extends AbstractValidator
56
{
57
    /**
58
     * Request method vector
59
     *
60
     * @var string[]
61
     */
62
    protected $methodVector = null;
63
    /**
64
     * Request submission times
65
     *
66
     * @var float[]
67
     */
68
    protected $submissionTimes = null;
69
    /**
70
     * Validation order position
71
     *
72
     * @var int
73
     */
74
    const POSITION = 100;
75
    /**
76
     * GET request
77
     *
78
     * @var string
79
     */
80
    const METHOD_GET = 'GET';
81
    /**
82
     * POST request
83
     *
84
     * @var string
85
     */
86
    const METHOD_POST = 'POST';
87
    /**
88
     * Minimum submission time
89
     *
90
     * @var float
91
     */
92
    const MINIMUM_SUBMISSION = 3;
93
    /**
94
     * Minimum submission time for follow-up submissions
95
     *
96
     * @var float
97
     */
98
    const MINIMUM_FOLLOWUP_SUBMISSION = 1;
99
    /**
100
     * Maximum submission time
101
     *
102
     * @var float
103
     */
104
    const MAXIMUM_SUBMISSION = 3600;
105
    /**
106
     * Block access
107
     *
108
     * @var string
109
     */
110
    const BLOCK = 'BLOCK';
111
112
    /**
113
     * Set the request method vector
114
     *
115
     * @param string $previous Previous request
116
     * @param string $current  Current request
117
     */
118 2
    public function setMethodVector(string $previous = null, string $current = null): void
119
    {
120
        // If the request method vector should be unset
121 2
        if ($previous === null) {
122
            $this->methodVector = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,string> of property $methodVector.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
123
124
            return;
125
        }
126
127 2
        $this->methodVector = [$this->validateRequestMethod($previous), $this->validateRequestMethod($current)];
128 2
    }
129
130
    /**
131
     * Sanitize and validate a request method
132
     *
133
     * @param string $method Request method
134
     *
135
     * @return string Validated request method
136
     * @throws InvalidArgumentException If the request method is invalid
137
     */
138 2
    protected function validateRequestMethod(string $method): string
139
    {
140 2
        $method = strtoupper($method);
141 2
        if ($method !== static::METHOD_GET && $method !== static::METHOD_POST) {
142
            throw new InvalidArgumentException(
143
                sprintf(InvalidArgumentException::INVALID_REQUEST_METHOD_STR, $method),
144
                InvalidArgumentException::INVALID_REQUEST_METHOD
145
            );
146
        }
147
148 2
        return $method;
149
    }
150
151
    /**
152
     * Sanitize and set the submission times
153
     *
154
     * @param float $max              Maximum submission time
155
     * @param float $min              Minimum submission time
156
     * @param float|null $minFollowUp Minimum submission time for follow-up submissions
157
     */
158 2
    public function setSubmissionTimes(float $max = null, float $min = null, float $minFollowUp = null): void
159
    {
160
        // If the submission times should be unset
161 2
        if ($max === null) {
162
            $this->submissionTimes = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,double> of property $submissionTimes.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
163
164
            return;
165
        }
166
167 2
        $max                   = min(floatval($max), static::MAXIMUM_SUBMISSION);
168 2
        $min                   = max(floatval($min), static::MINIMUM_SUBMISSION);
169 2
        $minFollowUp           = ($minFollowUp === null)
170 2
            ? $min : max(floatval($minFollowUp), static::MINIMUM_FOLLOWUP_SUBMISSION);
171 2
        $this->submissionTimes = [$min, $minFollowUp, $max];
0 ignored issues
show
Documentation Bug introduced by
It seems like array($min, $minFollowUp, $max) of type array<integer,double|int...,"2":"double|integer"}> is incompatible with the declared type array<integer,double> of property $submissionTimes.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
172 2
    }
173
174
    /**
175
     * Validate a request
176
     *
177
     * @param ServerRequestInterface $request Request
178
     * @param Antibot $antibot                Antibot instance
179
     *
180
     * @return bool
181
     * @throws HmacValidationException
182
     * @throws SkippedValidationException If no Antibot data has been submitted
183
     */
184 3
    public function validate(ServerRequestInterface $request, Antibot $antibot): bool
185
    {
186 3
        $data = $antibot->getData();
187
188
        // If no Antibot data has been submitted
189 3
        if ($data === null) {
190 2
            throw new SkippedValidationException(static::class);
191
        }
192
193 3
        return empty($data['hmac']) ? false : $this->validateHmac($data['hmac'], $request, $antibot);
194
    }
195
196
    /**
197
     * Create protective form HTML
198
     *
199
     * @param ServerRequestInterface $request Request
200
     * @param Antibot $antibot                Antibot instance
201
     *
202
     * @return InputElement[] HMTL input elements
203
     */
204 4
    public function armor(ServerRequestInterface $request, Antibot $antibot): array
205
    {
206 4
        $now   = null;
207 4
        $hmac  = $this->calculateHmac($request, $antibot, $now);
208
        $armor = [
209 4
            new InputElement([
210 4
                'type'  => 'hidden',
211 4
                'name'  => $antibot->getParameterPrefix().'[hmac]',
212 4
                'value' => $hmac
213
            ])
214
        ];
215
        // Add the timestamp field
216 4
        if ($now !== null) {
217 2
            $armor[] = new InputElement([
218 2
                'type'  => 'hidden',
219 2
                'name'  => $antibot->getParameterPrefix().'[ts]',
220 2
                'value' => intval($now)
221
            ]);
222
        }
223
224 4
        return $armor;
225
    }
226
227
    /**
228
     * Decrypt and validate an HMAC
229
     *
230
     * @param string $hmac                    HMAC
231
     * @param ServerRequestInterface $request Request
232
     * @param Antibot $antibot                Antibot instance
233
     *
234
     * @return bool HMAC is valid
235
     * @throws HmacValidationException If the request timing is invalid
236
     */
237 3
    protected function validateHmac(string $hmac, ServerRequestInterface $request, Antibot $antibot): bool
238
    {
239 3
        $previousMethod = null;
0 ignored issues
show
Unused Code introduced by
$previousMethod is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
240 3
        $hmacParams     = [$antibot->getUnique()];
241
242
        // Short-circuit blocked HMAC
243 3
        $hmacBlock   = $hmacParams;
244 3
        $hmacBlock[] = self::BLOCK;
245 3
        if (HmacFactory::createFromString(serialize($hmacBlock), $antibot->getUnique()) === $hmac) {
246
            return false;
247
        }
248
249
        // Validate the request method vector
250 3
        $this->validateRequestMethodVector($request, $hmacParams);
251
252
        // If the request timings validate
253 3
        if ($this->validateRequestTiming($hmac, $antibot, $hmacParams)) {
254 1
            return true;
255
        }
256
257
        // Else: Do a simple validation without request timings
258 2
        $currentHMAC = HmacFactory::createFromString(serialize($hmacParams), $antibot->getUnique());
259
260 2
        return $hmac === $currentHMAC;
261
    }
262
263
    /**
264
     * Validate the request method vector
265
     *
266
     * @param ServerRequestInterface $request Request
267
     * @param array $hmacParams               HMAC parameters
268
     *
269
     * @throws HmacValidationException If the request method order is invalid
270
     */
271 3
    protected function validateRequestMethodVector(ServerRequestInterface $request, array &$hmacParams): void
272
    {
273
        // If the request method vector should be used
274 3
        if (!empty($this->methodVector)) {
275 1
            $serverParams  = $request->getServerParams();
276 1
            $requestMethod = empty($serverParams['REQUEST_METHOD']) ? 'EMPTY' : $serverParams['REQUEST_METHOD'];
277 1
            if ($requestMethod !== $this->methodVector[1]) {
278 1
                throw new HmacValidationException(
279 1
                    HmacValidationException::INVALID_REQUEST_METHOD_ORDER_STR,
280 1
                    HmacValidationException::INVALID_REQUEST_METHOD_ORDER
281
                );
282
            }
283
284 1
            $hmacParams[] = $this->methodVector[0];
285
        }
286 3
    }
287
288
    /**
289
     * Validate the request timing
290
     *
291
     * @param string $hmac      HMAC
292
     * @param Antibot $antibot  Antibot instance
293
     * @param array $hmacParams HMAC parameters
294
     *
295
     * @return bool Request timings were enabled and validated successfully
296
     *
297
     * @throws HmacValidationException If the request timing is invalid
298
     */
299 3
    protected function validateRequestTiming(string $hmac, Antibot $antibot, array $hmacParams): bool
300
    {
301
        // If submission time checks are enabled
302 3
        if (!empty($this->submissionTimes)) {
303 1
            list($first, $min, $max) = $this->submissionTimes;
304 1
            $now       = time();
305 1
            $initial   = $now - $first;
306 1
            $delay     = null;
0 ignored issues
show
Unused Code introduced by
$delay is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
307 1
            $data      = $antibot->getData();
308 1
            $timestamp = empty($data['ts']) ? null : $data['ts'];
309
310
            // If a timestamp has been submitted
311 1
            if ($timestamp
312 1
                && (($timestamp + $min) <= $now)
313 1
                && (($timestamp + $max) >= $now)
314 1
                && $this->probeTimedHmacAsInitialAndFollowup($hmac, $antibot, $hmacParams, $timestamp, $initial)
315
            ) {
316 1
                $antibot->getLogger()->debug("[HMAC] Validated using submitted timestamp $timestamp");
317
318 1
                return true;
319
            } else {
320
                // Run through the valid seconds range
321 1
                for ($time = $now - $min; $time >= $now - $max; --$time) {
322
                    // If the HMAC validates as initial or follow-up request
323 1
                    if ($this->probeTimedHmacAsInitialAndFollowup($hmac, $antibot, $hmacParams, $time, $initial)) {
324
                        return true;
325
                    }
326
                }
327
            }
328
329 1
            throw new HmacValidationException(
330 1
                HmacValidationException::INVALID_REQUEST_TIMING_STR,
331 1
                HmacValidationException::INVALID_REQUEST_TIMING
332
            );
333
        }
334
335 2
        return false;
336
    }
337
338
    /**
339
     * Probe a timed HMAC both as initial and follow-up request
340
     *
341
     * @param string $hmac      HMAC
342
     * @param Antibot $antibot  Antibot instance
343
     * @param array $hmacParams HMAC params
344
     * @param int $timestamp    Timestamp
345
     * @param int $initial      Initial request threshold
346
     *
347
     * @return bool HMAC is valid
348
     */
349 1
    protected function probeTimedHmacAsInitialAndFollowup(
350
        string $hmac,
351
        Antibot $antibot,
352
        array $hmacParams,
353
        int $timestamp,
354
        int $initial
355
    ): bool {
356
        // If the HMAC validates with auto-guessed mode: Succeed
357 1
        if ($this->probeTimedHmac($hmac, $antibot, $hmacParams, $timestamp, $timestamp > $initial)) {
358 1
            return true;
359
        }
360
361
        // Also test as late follow-up request
362 1
        if (($timestamp <= $initial) && $this->probeTimedHMAC($hmac, $antibot, $hmacParams, $timestamp, true)) {
363
            return true;
364
        }
365
366 1
        return false;
367
    }
368
369
    /**
370
     * Probe a timed HMAC
371
     *
372
     * @param string $hmac      HMAC
373
     * @param Antibot $antibot  Antibot instance
374
     * @param array $hmacParams HMAC params
375
     * @param int $timestamp    Timestamp
376
     * @param bool $followUp    Is a follow-up request
377
     *
378
     * @return bool HMAC is valid
379
     */
380 1
    protected function probeTimedHmac(
381
        string $hmac,
382
        Antibot $antibot,
383
        array $hmacParams,
384
        int $timestamp,
385
        bool $followUp = false
386
    ): bool {
387 1
        if ($followUp) {
388 1
            $hmacParams[] = true;
389
        }
390 1
        $hmacParams[] = $timestamp;
391 1
        $currentHMAC  = HmacFactory::createFromString(serialize($hmacParams), $antibot->getUnique());
392
393 1
        $antibot->getLogger()->debug("[HMAC] Probing $timestamp (".($followUp ? 'FLLW' : 'INIT')."): $currentHMAC");
394
395 1
        return $currentHMAC == $hmac;
396
    }
397
398
    /**
399
     * Calculate the HMAC
400
     *
401
     * @param ServerRequestInterface $request Request
402
     * @param Antibot $antibot                Antibot instance
403
     * @param int|null $now                   Current timestamp
404
     *
405
     * @return string HMAC
406
     */
407 4
    protected function calculateHmac(ServerRequestInterface $request, Antibot $antibot, int &$now = null): string
408
    {
409 4
        $hmacParams = [$antibot->getUnique()];
410 4
        $now        = null;
411
412
        // Invalidate the HMAC if there's a current, invalid one
413 4
        if (false) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
414
415
        } else {
416 4
            $serverParams = $request->getServerParams();
417
418
            // If the request method vector should be used
419 4
            if (!empty($this->methodVector)) {
420 2
                $requestMethod = empty($serverParams['REQUEST_METHOD']) ? '' : $serverParams['REQUEST_METHOD'];
421 2
                $hmacParams[]  = $this->validateRequestMethod($requestMethod);
422
            }
423
424
            // If submission time checks are enabled
425 4
            if (!empty($this->submissionTimes)) {
426 2
                if (!empty($antibot->getData())) {
427 1
                    $hmacParams[] = true;
428
                }
429 2
                $hmacParams[] = $now = time();
430
            }
431
        }
432
433 4
        $hmac = HmacFactory::createFromString(serialize($hmacParams), $antibot->getUnique());
434
435 4
        $antibot->getLogger()->debug("[HMAC] Created HMAC $hmac", $hmacParams);
436
437 4
        return $hmac;
438
    }
439
}
440