Passed
Pull Request — master (#19)
by Jitendra
01:47
created

Throttle::getRetryKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
1
<?php
2
3
namespace PhalconExt\Http\Middleware;
4
5
use Phalcon\Http\Request;
6
use Phalcon\Http\Response;
7
use PhalconExt\Http\BaseMiddleware;
8
9
/**
10
 * A request throttling middleware.
11
 *
12
 * @author  Jitendra Adhikari <[email protected]>
13
 * @license MIT
14
 *
15
 * @link    https://github.com/adhocore/phalcon-ext
16
 */
17
class Throttle extends BaseMiddleware
18
{
19
    /** @var string */
20
    protected $redis;
21
22
    protected $configKey = 'throttle';
23
24
    protected $retryKey = '';
25
26
    /**
27
     * Get retry key that causes throttling.
28
     *
29
     * @return string
30
     */
31
    public function getRetryKey(): string
32
    {
33
        return $this->retryKey;
34
    }
35
36
    /**
37
     * Handle the throttle.
38
     *
39
     * @param Request  $request
40
     * @param Response $response
41
     *
42
     * @return bool
43
     */
44
    public function before(Request $request, Response $response): bool
45
    {
46
        if ('' === $this->retryKey = $this->findRetryKey($request)) {
47
            return true;
48
        }
49
50
        $this->disableView();
51
52
        $after = \ceil($this->di('redis')->getTtl($this->retryKey) / 60);
53
54
        $response
55
            ->setContent("Too many requests. Try again in $after min.")
56
            ->setHeader('Retry-After', $after)
57
            ->setStatusCode(429)
58
            ->send();
59
60
        return false;
61
    }
62
63
    /**
64
     * Find the redis key that contains hits counter which has exceeded threshold for throttle.
65
     *
66
     * @param Request $request
67
     *
68
     * @return string
69
     */
70
    protected function findRetryKey(Request $request): string
71
    {
72
        $retryKey = '';
73
        $redis    = $this->di('redis');
74
        $baseKey  = $this->getKey($request);
75
76
        foreach ($this->config['maxHits'] as $minutes => $maxHits) {
77
            $key = "$baseKey:$minutes";
78
79
            if ($this->shouldThrottle($redis, $key, $minutes, $maxHits)) {
80
                $retryKey = $key;
81
            }
82
        }
83
84
        return $retryKey;
85
    }
86
87
    /**
88
     * Get the unique key for this client.
89
     *
90
     * @param Request $request
91
     *
92
     * @return string
93
     */
94
    protected function getKey(Request $request): string
95
    {
96
        $key = $request->getClientAddress(true);
97
98
        if ($this->config['checkUserAgent'] ?? false) {
99
            $key .= ':' . \md5($request->getUserAgent());
100
        }
101
102
        return ($this->config['prefix'] ?? '') . $key;
103
    }
104
105
    /**
106
     * Check if we should throttle. Update hits counter if not.
107
     *
108
     * @param Request $request
109
     *
110
     * @return bool
111
     */
112
    protected function shouldThrottle($redis, string $key, int $minutes, int $maxHits): bool
113
    {
114
        $hits = $redis->get($key) ?: 0;
115
        $ttl  = $hits ? $redis->getTtl($key) : $minutes * 60;
116
117
        if ($hits >= $maxHits) {
118
            return true;
119
        }
120
121
        $redis->save($key, $hits + 1, $ttl);
122
123
        return false;
124
    }
125
}
126