1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* ================================== |
4
|
|
|
* Responsible PHP API |
5
|
|
|
* ================================== |
6
|
|
|
* |
7
|
|
|
* @link Git https://github.com/vince-scarpa/responsibleAPI.git |
8
|
|
|
* |
9
|
|
|
* @api Responible API |
10
|
|
|
* @package responsible\core\throttle |
11
|
|
|
* |
12
|
|
|
* @author Vince scarpa <[email protected]> |
13
|
|
|
* |
14
|
|
|
*/ |
15
|
|
|
namespace responsible\core\throttle; |
16
|
|
|
|
17
|
|
|
use responsible\core\exception; |
18
|
|
|
use responsible\core\throttle; |
19
|
|
|
use responsible\core\user; |
20
|
|
|
|
21
|
|
|
class limiter |
22
|
|
|
{ |
23
|
|
|
/** |
24
|
|
|
* [$capacity Bucket volume] |
25
|
|
|
* @var integer |
26
|
|
|
*/ |
27
|
|
|
private $capacity = 100; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* [$leakRate Constant rate at which the bucket will leak] |
31
|
|
|
* @var string|integer |
32
|
|
|
*/ |
33
|
|
|
private $leakRate = 1; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* [$unpacked] |
37
|
|
|
*/ |
38
|
|
|
private $unpacked; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* [$packed] |
42
|
|
|
* @var string |
43
|
|
|
*/ |
44
|
|
|
private $packed; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* [$bucket] |
48
|
|
|
*/ |
49
|
|
|
private $bucket; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* [$account User account object] |
53
|
|
|
*/ |
54
|
|
|
private $account; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* [$options Responisble options] |
58
|
|
|
*/ |
59
|
|
|
private $options; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* [$timeframe Durations are in seconds] |
63
|
|
|
* @var array |
64
|
|
|
*/ |
65
|
|
|
private static $timeframe = [ |
66
|
|
|
'SECOND' => 1, |
67
|
|
|
'MINUTE' => 60, |
68
|
|
|
'HOUR' => 3600, |
69
|
|
|
'DAY' => 86400, |
70
|
|
|
'CUSTOM' => 0, |
71
|
|
|
]; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* [$window Timeframe window] |
75
|
|
|
* @var integer|string |
76
|
|
|
*/ |
77
|
|
|
private $window = 'MINUTE'; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* [$unlimited Rate limiter bypass if true] |
81
|
|
|
* @var boolean |
82
|
|
|
*/ |
83
|
|
|
private $unlimited = false; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* [$scope Set the default scope] |
87
|
|
|
* @var string |
88
|
|
|
*/ |
89
|
|
|
private $scope = 'private'; |
90
|
|
|
|
91
|
|
|
public function __construct($limit = null, $rate = null) |
92
|
|
|
{ |
93
|
|
|
if (!is_null($limit)) { |
94
|
|
|
$this->capacity = $limit; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
if (!is_null($rate)) { |
98
|
|
|
$this->window = $rate; |
99
|
|
|
} |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* [setupOptions Set any Responsible API options] |
104
|
|
|
* @return self |
105
|
|
|
*/ |
106
|
|
|
public function setupOptions() |
107
|
|
|
{ |
108
|
|
|
$options = $this->getOptions(); |
109
|
|
|
|
110
|
|
|
$this->setCapacity($options); |
111
|
|
|
|
112
|
|
|
$this->setTimeframe($options); |
113
|
|
|
|
114
|
|
|
$this->setLeakRate($options); |
115
|
|
|
|
116
|
|
|
$this->setUnlimited($options); |
117
|
|
|
|
118
|
|
|
if (isset($this->account->scope) && |
119
|
|
|
($this->account->scope == 'anonymous' || $this->account->scope == 'public') |
120
|
|
|
) { |
121
|
|
|
$this->scope = $this->account->scope; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
return $this; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* [throttleRequest Build the Responsible API throttle] |
129
|
|
|
* @return boolean|void |
130
|
|
|
*/ |
131
|
|
|
public function throttleRequest() |
132
|
|
|
{ |
133
|
|
|
if ($this->isUnlimited() || $this->scope !== 'private') { |
134
|
|
|
return true; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$account = $this->getAccount(); |
138
|
|
|
|
139
|
|
|
if (empty($account)) { |
140
|
|
|
return false; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* [$unpack Unpack the account bucket data] |
145
|
|
|
*/ |
146
|
|
|
$this->unpacked = (new throttle\tokenPack)->unpack( |
147
|
|
|
$account->bucket |
148
|
|
|
); |
149
|
|
|
if (empty($this->unpacked)) { |
150
|
|
|
$this->unpacked = array( |
151
|
|
|
'drops' => 1, |
152
|
|
|
'time' => $account->access, |
153
|
|
|
); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
$this->bucket = (new throttle\tokenBucket()) |
157
|
|
|
->setTimeframe($this->getTimeframe()) |
158
|
|
|
->setCapacity($this->getCapacity()) |
159
|
|
|
->setLeakRate($this->getLeakRate()) |
160
|
|
|
->pour($this->unpacked['drops'], $this->unpacked['time']) |
161
|
|
|
; |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Check if the bucket still has capacity to fill |
165
|
|
|
*/ |
166
|
|
|
if ($this->bucket->capacity()) { |
167
|
|
|
$this->bucket->pause(false); |
168
|
|
|
$this->bucket->fill(); |
169
|
|
|
} else { |
170
|
|
|
if ($this->getLeakRate() <= 0) { |
171
|
|
|
if ($this->unpacked['pauseAccess'] == false) { |
172
|
|
|
$this->bucket->pause(true); |
173
|
|
|
$this->save(); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
if ($this->bucket->refill($account->access)) { |
177
|
|
|
$this->save(); |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
(new exception\errorException)->error('TOO_MANY_REQUESTS'); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
$this->save(); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* [updateBucket Store the buckets token data and user access time] |
189
|
|
|
* @return void |
190
|
|
|
*/ |
191
|
|
|
private function save() |
192
|
|
|
{ |
193
|
|
|
$this->packed = (new throttle\tokenPack)->pack( |
194
|
|
|
$this->bucket->getTokenData() |
195
|
|
|
); |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* [Update account access] |
199
|
|
|
*/ |
200
|
|
|
(new user\user) |
201
|
|
|
->setAccountID($this->getAccount()->account_id) |
202
|
|
|
->setBucketToken($this->packed) |
203
|
|
|
->updateAccountAccess() |
204
|
|
|
; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* [getThrottle Return a list of the throttled results] |
209
|
|
|
* @return array |
210
|
|
|
*/ |
211
|
|
|
public function getThrottle() |
212
|
|
|
{ |
213
|
|
|
if ($this->isUnlimited() || $this->scope !== 'private') { |
214
|
|
|
return array( |
215
|
|
|
'unlimited' => true, |
216
|
|
|
); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
$windowFrame = (is_string($this->getTimeframe())) |
220
|
|
|
? $this->getTimeframe() |
221
|
|
|
: $this->getTimeframe() . 'secs' |
222
|
|
|
; |
223
|
|
|
|
224
|
|
|
if (is_null($this->bucket)) { |
225
|
|
|
return; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
return array( |
229
|
|
|
'limit' => $this->getCapacity(), |
230
|
|
|
'leakRate' => $this->getLeakRate(), |
231
|
|
|
'leak' => $this->bucket->getLeakage(), |
232
|
|
|
'lastAccess' => $this->getLastAccessDate(), |
233
|
|
|
'description' => $this->getCapacity() . ' requests per ' . $windowFrame, |
234
|
|
|
'bucket' => $this->bucket->getTokenData(), |
235
|
|
|
); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* [getLastAccessDate Get the last recorded access in date format] |
240
|
|
|
* @return string |
241
|
|
|
*/ |
242
|
|
|
private function getLastAccessDate() |
243
|
|
|
{ |
244
|
|
|
if (isset($this->bucket->getTokenData()['time'])) { |
245
|
|
|
return date('m/d/y h:i:sa', $this->bucket->getTokenData()['time']); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
return 'Can\'t be converted'; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* [setAccount Set the requests account] |
253
|
|
|
* @return self |
254
|
|
|
*/ |
255
|
|
|
public function setAccount($account) |
256
|
|
|
{ |
257
|
|
|
$this->account = $account; |
258
|
|
|
return $this; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* [getAccount Get the requests account] |
263
|
|
|
*/ |
264
|
|
|
public function getAccount() |
265
|
|
|
{ |
266
|
|
|
if (is_null($this->account)) { |
267
|
|
|
(new exception\errorException) |
268
|
|
|
->setOptions($this->getOptions()) |
269
|
|
|
->error('UNAUTHORIZED'); |
270
|
|
|
return; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
return $this->account; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* [options Responsible API options] |
278
|
|
|
* @param array $options |
279
|
|
|
*/ |
280
|
|
|
public function options($options) |
281
|
|
|
{ |
282
|
|
|
$this->options = $options; |
283
|
|
|
return $this; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* [getOptions Get the stored Responsible API options] |
288
|
|
|
* @return array |
289
|
|
|
*/ |
290
|
|
|
private function getOptions() |
291
|
|
|
{ |
292
|
|
|
return $this->options; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* [hasOptionProperty Check if an option property is set] |
297
|
|
|
* @param array $options |
298
|
|
|
* @param string $property |
299
|
|
|
* @return string|integer|boolean |
300
|
|
|
*/ |
301
|
|
|
private function hasOptionProperty(array $options, $property, $default = false) |
302
|
|
|
{ |
303
|
|
|
$val = isset($options[$property]) ? $options[$property] : $default; |
304
|
|
|
|
305
|
|
|
if ($val && empty($options[$property])) { |
306
|
|
|
$val = $default; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
return $val; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* [setCapacity Set the buckets capacity] |
314
|
|
|
* @param array $options |
315
|
|
|
*/ |
316
|
|
|
public function setCapacity($options) |
317
|
|
|
{ |
318
|
|
|
$hasCapacityOption = $this->hasOptionProperty($options, 'rateLimit'); |
319
|
|
|
|
320
|
|
|
if ($hasCapacityOption) { |
321
|
|
|
if (!is_integer($hasCapacityOption) || empty($hasCapacityOption)) { |
322
|
|
|
$hasCapacityOption = false; |
323
|
|
|
} |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
$this->capacity = ($hasCapacityOption) ? $hasCapacityOption : $this->capacity; |
|
|
|
|
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* [getCapacity Get the buckets capacity] |
331
|
|
|
* @return integer |
332
|
|
|
*/ |
333
|
|
|
public function getCapacity() |
334
|
|
|
{ |
335
|
|
|
return $this->capacity; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* [setTimeframe Set the window timeframe] |
340
|
|
|
* @param array $options |
341
|
|
|
*/ |
342
|
|
|
public function setTimeframe($options) |
343
|
|
|
{ |
344
|
|
|
$timeframe = $this->hasOptionProperty($options, 'rateWindow'); |
345
|
|
|
|
346
|
|
|
if (!is_string($timeframe) && !is_numeric($timeframe)) { |
347
|
|
|
$timeframe = $this->window; |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
if (!$timeframe) { |
351
|
|
|
$timeframe = $this->window; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
if (is_numeric($timeframe)) { |
355
|
|
|
self::$timeframe['CUSTOM'] = $timeframe; |
356
|
|
|
$this->window = self::$timeframe['CUSTOM']; |
357
|
|
|
return; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
if (isset(self::$timeframe[$timeframe])) { |
361
|
|
|
$this->window = self::$timeframe[$timeframe]; |
362
|
|
|
return; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
$this->window = self::$timeframe['MINUTE']; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* [getTimeframe Get the timeframe window] |
370
|
|
|
* @return integer|string |
371
|
|
|
*/ |
372
|
|
|
public function getTimeframe() |
373
|
|
|
{ |
374
|
|
|
return $this->window; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* [setLeakRate Set the buckets leak rate] |
379
|
|
|
* Options: slow, medium, normal, default, fast or custom positive integer |
380
|
|
|
* @param array $options |
381
|
|
|
*/ |
382
|
|
|
private function setLeakRate($options) |
383
|
|
|
{ |
384
|
|
|
if (isset($options['leak']) && !$options['leak']) { |
385
|
|
|
$options['leakRate'] = 'default'; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
$leakRate = $this->hasOptionProperty($options, 'leakRate'); |
389
|
|
|
|
390
|
|
|
if (empty($leakRate) || !is_string($leakRate)) { |
391
|
|
|
$leakRate = 'default'; |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
$this->leakRate = $leakRate; |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* [getLeakRate Get the buckets leak rate] |
399
|
|
|
* @return string|integer |
400
|
|
|
*/ |
401
|
|
|
private function getLeakRate() |
402
|
|
|
{ |
403
|
|
|
return $this->leakRate; |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
/** |
407
|
|
|
* [setUnlimited Rate limiter bypass] |
408
|
|
|
* @param array $options |
409
|
|
|
*/ |
410
|
|
|
private function setUnlimited($options) |
411
|
|
|
{ |
412
|
|
|
$unlimited = false; |
413
|
|
|
|
414
|
|
|
if (isset($options['unlimited']) && ($options['unlimited'] == 1 || $options['unlimited'] == true)) { |
415
|
|
|
$unlimited = true; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
if (isset($options['requestType']) && $options['requestType'] === 'debug') { |
419
|
|
|
$unlimited = true; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
$this->unlimited = $unlimited; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
/** |
426
|
|
|
* [isUnlimited Check if the Responsible API is set to unlimited] |
427
|
|
|
* @return boolean |
428
|
|
|
*/ |
429
|
|
|
private function isUnlimited() |
430
|
|
|
{ |
431
|
|
|
return $this->unlimited; |
432
|
|
|
} |
433
|
|
|
} |
434
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.