1 | <?php |
||
2 | declare(strict_types=1); |
||
3 | defined('BASEPATH') OR exit('No direct script access allowed'); |
||
4 | |||
5 | require_once('RESTAuth.php'); |
||
6 | require_once('RESTResponse.php'); |
||
7 | require_once('RESTExceptions.php'); |
||
8 | |||
9 | class REST |
||
10 | { |
||
11 | /** |
||
12 | * [private description] |
||
13 | * @var [type] |
||
0 ignored issues
–
show
Documentation
Bug
introduced
by
Loading history...
|
|||
14 | */ |
||
15 | private $ci; |
||
16 | |||
17 | /** |
||
18 | * [private description] |
||
19 | * @var [type] |
||
0 ignored issues
–
show
|
|||
20 | */ |
||
21 | private $api_key_limit_column; |
||
22 | |||
23 | /** |
||
24 | * [private description] |
||
25 | * @var [type] |
||
0 ignored issues
–
show
|
|||
26 | */ |
||
27 | private $api_key_column; |
||
28 | |||
29 | /** |
||
30 | * [private description] |
||
31 | * @var [type] |
||
0 ignored issues
–
show
|
|||
32 | */ |
||
33 | private $per_hour; |
||
34 | |||
35 | /** |
||
36 | * [private description] |
||
37 | * @var [type] |
||
0 ignored issues
–
show
|
|||
38 | */ |
||
39 | private $ip_per_hour; |
||
40 | |||
41 | /** |
||
42 | * [private description] |
||
43 | * @var [type] |
||
0 ignored issues
–
show
|
|||
44 | */ |
||
45 | private $show_header; |
||
46 | |||
47 | /** |
||
48 | * [private description] |
||
49 | * @var [type] |
||
0 ignored issues
–
show
|
|||
50 | */ |
||
51 | private $whitelist; |
||
52 | |||
53 | /** |
||
54 | * [private description] |
||
55 | * @var [type] |
||
0 ignored issues
–
show
|
|||
56 | */ |
||
57 | private $checked_rate_limit = false; |
||
58 | |||
59 | /** |
||
60 | * [private description] |
||
61 | * @var [type] |
||
0 ignored issues
–
show
|
|||
62 | */ |
||
63 | private $header_prefix; |
||
64 | |||
65 | /** |
||
66 | * [private description] |
||
67 | * @var [type] |
||
0 ignored issues
–
show
|
|||
68 | */ |
||
69 | private $limit_api; |
||
70 | |||
71 | /** |
||
72 | * [public description] |
||
73 | * @var [type] |
||
0 ignored issues
–
show
|
|||
74 | */ |
||
75 | public $userId; |
||
76 | |||
77 | /** |
||
78 | * [public description] |
||
79 | * @var [type] |
||
0 ignored issues
–
show
|
|||
80 | */ |
||
81 | public $apiKey; |
||
82 | |||
83 | /** |
||
84 | * [public description] |
||
85 | * @var [type] |
||
0 ignored issues
–
show
|
|||
86 | */ |
||
87 | public $apiKeyHeader; |
||
88 | |||
89 | /** |
||
90 | * [public description] |
||
91 | * @var [type] |
||
0 ignored issues
–
show
|
|||
92 | */ |
||
93 | public $token; |
||
94 | |||
95 | /** |
||
96 | * [public description] |
||
97 | * @var [type] |
||
0 ignored issues
–
show
|
|||
98 | */ |
||
99 | public $allowedIps; |
||
100 | |||
101 | /** |
||
102 | * [public description] |
||
103 | * @var [type] |
||
0 ignored issues
–
show
|
|||
104 | */ |
||
105 | public $config; |
||
106 | |||
107 | /** |
||
108 | * [public description] |
||
109 | * @var [type] |
||
0 ignored issues
–
show
|
|||
110 | */ |
||
111 | public $authPreempted = false; |
||
112 | |||
113 | /** |
||
114 | * [PACKAGE description] |
||
115 | * @var string |
||
116 | */ |
||
117 | const PACKAGE = "francis94c/ci-rest"; |
||
118 | |||
119 | /** |
||
120 | * [RATE_LIMIT description] |
||
121 | * @var string |
||
122 | */ |
||
123 | const RATE_LIMIT = "RateLimit"; |
||
124 | |||
125 | /** |
||
126 | * [AUTH_GRAVITY description] |
||
127 | * @var integer |
||
128 | */ |
||
129 | const AUTH_GRAVITY = 0b100; |
||
130 | const AUTH_PASSIVE = 0b010; |
||
131 | const AUTH_FINAL = 0b001; |
||
132 | |||
133 | /** |
||
134 | * [__construct This is the part of the code that takes care of all |
||
135 | * authentiations. allowing you to focus on building wonderful things at REST. |
||
136 | * pun intended ;-)] |
||
137 | * @param array|null $params Initialization parameters from the Slint system. |
||
138 | * There's no use for this arg yet. |
||
139 | */ |
||
140 | function __construct(?array $params=null) |
||
141 | { |
||
142 | $this->ci =& get_instance(); |
||
143 | |||
144 | if ($this->ci->input->is_cli_request()) return; |
||
145 | |||
146 | // Load Config If Exists. |
||
147 | //$this->ci->config->load('rest', true, true); |
||
148 | if (is_file(APPPATH . 'config/rest.php')) { |
||
149 | include APPPATH . 'config/rest.php'; |
||
150 | } else { |
||
151 | $config = []; |
||
152 | } |
||
153 | |||
154 | $this->config = $config; |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
155 | |||
156 | // Load Database. |
||
157 | $this->ci->load->database(); |
||
158 | |||
159 | // load URL Helper |
||
160 | $this->ci->load->helper('url'); |
||
161 | |||
162 | // Load REST Helper. |
||
163 | $this->ci->load->splint(self::PACKAGE, '%rest'); |
||
164 | |||
165 | // Load Model. |
||
166 | $this->ci->load->splint(self::PACKAGE, '*RESTModel', 'rest_model'); |
||
167 | $this->rest_model =& $this->ci->rest_model; |
||
0 ignored issues
–
show
|
|||
168 | |||
169 | $this->rest_model->init([ |
||
170 | 'users_table' => $config['basic_auth']['users_table'] ?? null, |
||
171 | 'users_id_column' => $config['basic_auth']['id_column'] ?? null, |
||
172 | 'users_username_column' => $config['basic_auth']['username_column'] ?? null, |
||
173 | 'users_email_column' => $config['basic_auth']['email_column'] ?? null, |
||
174 | 'users_password_column' => $config['basic_auth']['password_column'] ?? null, |
||
175 | 'api_key_table' => $config['api_key_auth']['api_key_table'] ?? null, |
||
176 | 'api_key_column' => $config['api_key_auth']['api_key_column'] ?? null, |
||
177 | 'api_key_limit_column' => $config['api_key_auth']['api_key_limit_column'] ?? null |
||
178 | ]); |
||
179 | |||
180 | // Load Variable(s) from Config. |
||
181 | $this->allowedIps = $config['allowed_ips'] ?? ['127.0.0.1', '[::1]']; |
||
182 | $this->apiKeyHeader = $config['api_key_header'] ?? 'X-API-KEY'; |
||
183 | $this->api_key_limit_column = $config['api_key_auth']['api_key_limit_column'] ?? null; |
||
184 | $this->api_key_column = $config['api_key_auth']['api_key_column'] ?? null; |
||
185 | $this->limit_api = $config['api_limiter']['api_limiter'] ?? false; |
||
186 | $this->per_hour = $config['api_limiter']['per_hour'] ?? 100; |
||
187 | $this->ip_per_hour = $config['api_limiter']['ip_per_hour'] ?? 50; |
||
188 | $this->show_header = $config['api_limiter']['show_header'] ?? null; |
||
189 | $this->whitelist = $config['api_limiter']['whitelist'] ?? []; |
||
190 | $this->header_prefix = $config['api_limiter']['header_prefix'] ?? 'X-RateLimit-'; |
||
191 | |||
192 | // Limit Only? |
||
193 | //if ($this->config['api_limiter']['api_limit_only'] ?? false) { |
||
194 | //return; |
||
195 | //} |
||
196 | |||
197 | // Authenticate |
||
198 | if ($this->ci->uri->total_segments() > 0) { |
||
199 | $this->authenticate(); |
||
200 | } |
||
201 | |||
202 | // Generic Rate Limiter. |
||
203 | if ($this->limit_api && !$this->checked_rate_limit && |
||
204 | ($config['api_limiter']['limit_by_ip'] ?? false)) { |
||
205 | $this->api_rest_limit_by_ip_address(); |
||
206 | } |
||
207 | |||
208 | log_message('debug', 'REST Request Authenticated and REST Library Initialized.'); |
||
209 | } |
||
210 | |||
211 | /** |
||
212 | * [authenticate description] |
||
213 | * @date 2020-01-30 |
||
214 | */ |
||
215 | private function authenticate():void |
||
216 | { |
||
217 | $auths = null; |
||
218 | $auths = $this->config['auth'] ?? null; |
||
219 | if ($auths) $auths = is_array($auths) ? $auths : [$auths]; |
||
220 | |||
221 | if (!$auths) return; // No authentication(s) to carry out. |
||
222 | |||
223 | /** |
||
224 | * $this->process_auth() terminates the script if authentication fails |
||
225 | * It will call the callable in the rest.php config file under |
||
226 | * response_callbacks which matches the necesarry RESTResponse constant |
||
227 | * before exiting. Which callable is called in any situation is documented |
||
228 | * in README.md |
||
229 | */ |
||
230 | |||
231 | foreach ($auths as $key => $auth) { |
||
232 | if ($this->authPreempted) break; |
||
233 | if (is_numeric($key)) { |
||
234 | $this->process_auth($auth, self::AUTH_GRAVITY); |
||
235 | } else { |
||
236 | $this->process_auth($key, $auth); |
||
237 | } |
||
238 | } |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * [process_auth description] |
||
243 | * @date 2020-04-07 |
||
244 | * @param string $auth [description] |
||
245 | * @param int $flags [description] |
||
246 | */ |
||
247 | private function process_auth(string &$auth, int $flags):void |
||
248 | { |
||
249 | switch ($auth) { |
||
250 | case RESTAuth::IP: $this->ip_auth($flags); break; |
||
251 | case RESTAuth::BASIC: $this->basic_auth($flags); break; |
||
252 | case RESTAuth::API_KEY: $this->api_key_auth($flags); break; |
||
253 | case RESTAuth::OAUTH2: $this->bearer_auth(RESTAuth::OAUTH2, $flags); break; |
||
254 | case RESTAuth::BEARER: $this->bearer_auth(RESTAuth::BEARER, $flags); break; |
||
255 | case RESTAuth::SECRET: $this->bearer_auth(RESTAuth::SECRET, $flags); break; |
||
256 | default: $this->custom_auth($auth, $flags); |
||
257 | } |
||
258 | } |
||
259 | |||
260 | /** |
||
261 | * [auth_proceed description] |
||
262 | * @date 2020-04-07 |
||
263 | * @param bool $success [description] |
||
264 | * @param int $flags [description] |
||
265 | * @return bool [description] |
||
266 | */ |
||
267 | private function auth_proceed(bool $success, int $flags):bool |
||
268 | { |
||
269 | if ($flags & self::AUTH_GRAVITY) return $success; |
||
270 | if ($success) { |
||
271 | if ($flags & self::AUTH_FINAL) { |
||
272 | $this->authPreempted = true; |
||
273 | return true; |
||
274 | } |
||
0 ignored issues
–
show
The function implicitly returns
null when the if condition on line 271 is false . This is incompatible with the type-hinted return boolean . Consider adding a return statement or allowing null as return value.
For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example: interface ReturnsInt {
public function returnsIntHinted(): int;
}
class MyClass implements ReturnsInt {
public function returnsIntHinted(): int
{
if (foo()) {
return 123;
}
// here: null is implicitly returned
}
}
Loading history...
|
|||
275 | } else { |
||
276 | return $flags & self::AUTH_PASSIVE ? true : false; |
||
277 | } |
||
278 | } |
||
279 | |||
280 | /** |
||
281 | * [ip_auth description] |
||
282 | * @date 2020-04-07 |
||
283 | * @param int $flags [description] |
||
284 | */ |
||
285 | private function ip_auth(int $flags):void |
||
286 | { |
||
287 | if (!$this->auth_proceed(in_array($this->ci->input->ip_address(), $this->allowedIps), $flags)) { |
||
288 | $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::IP); // Exits. |
||
289 | } |
||
290 | } |
||
291 | |||
292 | /** |
||
293 | * [bearer_auth description] |
||
294 | * @date 2020-04-07 |
||
295 | * @param string $auth [description] |
||
296 | * @param int $flags [description] |
||
297 | */ |
||
298 | private function bearer_auth(string $auth, int $flags):void |
||
299 | { |
||
300 | $authorization = $this->get_authorization_header(); |
||
301 | $shouldProceed = $this->auth_proceed(false, $flags); |
||
302 | if ($authorization == null || substr_count($authorization, ' ') != 1) { |
||
303 | if ($shouldProceed) return; |
||
304 | $this->handle_response(RESTResponse::BAD_REQUEST, $auth, 'Bad Request'); // Exits. |
||
305 | } |
||
306 | $token = explode(" ", $authorization); |
||
307 | if ($token[0] != $auth) { |
||
308 | if ($shouldProceed) return; |
||
309 | $this->handle_response(RESTResponse::BAD_REQUEST, $auth, 'Bad Request'); // Exits. |
||
310 | } |
||
311 | $this->token = $token[1]; |
||
312 | // Call Up Custom Implemented Bearer/Token Authorization. |
||
313 | // Callback Check. |
||
314 | if (!isset($this->config['auth_callbacks'][$auth])) { |
||
315 | $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits. |
||
316 | } |
||
317 | // Authorization. |
||
318 | if (!$this->auth_proceed($this->config['auth_callbacks'][$auth]($this, $this->token), $flags)) { |
||
319 | $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits. |
||
320 | } |
||
321 | } |
||
322 | |||
323 | /** |
||
324 | * [basic_auth description] |
||
325 | * @date 2020-04-07 |
||
326 | * @param int $flags [description] |
||
327 | */ |
||
328 | private function basic_auth(int $flags):void |
||
329 | { |
||
330 | $username = $_SERVER['PHP_AUTH_USER'] ?? null; |
||
331 | $password = $_SERVER['PHP_AUTH_PW'] ?? null; |
||
332 | if (!$this->auth_proceed(!$username || !$password, $flags)) $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::BASIC); // Exits. |
||
333 | if (!$this->auth_proceed($this->rest_model->basicAuth($this, $username, $password), $flags)) $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::BASIC); // Exits. |
||
334 | } |
||
335 | /** |
||
336 | * [api_key_auth description] |
||
337 | */ |
||
338 | private function api_key_auth(int $flags=self::AUTH_GRAVITY):void |
||
339 | { |
||
340 | if (uri_string() == '') return; |
||
341 | $shouldProceed = $this->auth_proceed(false, $flags); |
||
342 | |||
343 | if (!$this->ci->input->get_request_header($this->apiKeyHeader, true) && !$shouldProceed) { |
||
344 | // if (!isset($_SERVER['HTTP_' . str_replace("-", "_", $this->apiKeyHeader)])) { |
||
345 | $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::API_KEY); // Exits. |
||
346 | } |
||
347 | |||
348 | $apiKey = $this->rest_model->getAPIKeyData( |
||
349 | $this->ci->input->get_request_header($this->apiKeyHeader, true) |
||
350 | ); |
||
351 | |||
352 | if ($apiKey == null && !$shouldProceed) { |
||
353 | $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::API_KEY); // Exits. |
||
354 | } |
||
355 | |||
356 | $this->apiKey = $apiKey; |
||
357 | |||
358 | if (!$this->auth_proceed(true, $flags)) return; |
||
359 | |||
360 | // ==== API KEY Auth Passed ==== // |
||
361 | |||
362 | if ($this->limit_api && $this->api_key_limit_column != null && $apiKey->{$this->api_key_limit_column} == 1) { |
||
363 | $this->limitAPIKey($apiKey->{$this->api_key_column}); |
||
364 | } |
||
365 | |||
366 | $this->checked_rate_limit = true; // Ignore Limit By IP. |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * [limitAPIKey description] |
||
371 | * @date 2020-04-08 |
||
372 | * @param string $apiKey [description] |
||
373 | */ |
||
374 | public function limitAPIKey(string $apiKey):void |
||
375 | { |
||
376 | // Trunctate Rate Limit Data. |
||
377 | $this->rest_model->truncateRatelimitData(); |
||
378 | // Check Whitelist. |
||
379 | if (in_array($this->ci->input->ip_address(), $this->whitelist)) { |
||
380 | $this->checked_rate_limit = true; // Ignore Limit By IP. |
||
381 | return; |
||
382 | } |
||
383 | // Should we acyually Limit? |
||
384 | if ($this->per_hour > 0) { |
||
385 | $client = hash('md5', $this->ci->input->ip_address() . "%" . $apiKey); |
||
386 | $limitData = $this->rest_model->getLimitData($client, '_api_keyed_user'); |
||
387 | if ($limitData == null) { |
||
388 | $limitData = []; |
||
389 | $limitData['count'] = 0; |
||
390 | $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60)); |
||
391 | $limitData['start'] = date('d M Y H:i:s'); |
||
392 | } |
||
393 | if ($this->per_hour - $limitData['count'] > 0) { |
||
394 | if (!$this->rest_model->insertLimitData($client, '_api_keyed_user')) { |
||
395 | $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits. |
||
396 | } |
||
397 | ++$limitData['count']; |
||
398 | if ($this->show_header) { |
||
399 | header($this->header_prefix.'Limit: '.$this->per_hour); |
||
400 | header($this->header_prefix.'Remaining: '.($this->per_hour - $limitData['count'])); |
||
401 | header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch'])); |
||
402 | } |
||
403 | } else { |
||
404 | header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s')))); |
||
405 | $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits. |
||
406 | } |
||
407 | } |
||
408 | } |
||
409 | |||
410 | /** |
||
411 | * [api_rest_limit_by_ip_address description] |
||
412 | * TODO: Implement. |
||
413 | */ |
||
414 | private function api_rest_limit_by_ip_address():void |
||
415 | { |
||
416 | // Trunctate Rate Limit Data. |
||
417 | $this->rest_model->truncateRatelimitData(); |
||
418 | // Check Whitelist. |
||
419 | if (in_array($this->ci->input->ip_address(), $this->whitelist)) return; |
||
420 | // Should we acyually Limit? |
||
421 | if ($this->ip_per_hour > 0) { |
||
422 | $client = hash('md5', $this->ci->input->ip_address()); |
||
423 | $limitData = $this->rest_model->getLimitData($client, '_ip_address'); |
||
424 | if ($limitData == null) { |
||
425 | $limitData = []; |
||
426 | $limitData['count'] = 0; |
||
427 | $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60)); |
||
428 | $limitData['start'] = date('d M Y H:i:s'); |
||
429 | } |
||
430 | if ($this->ip_per_hour - $limitData['count'] > 0) { |
||
431 | if (!$this->rest_model->insertLimitData($client, '_ip_address')) { |
||
432 | $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits. |
||
433 | } |
||
434 | ++$limitData['count']; |
||
435 | if ($this->show_header) { |
||
436 | header($this->header_prefix.'Limit: '.$this->ip_per_hour); |
||
437 | header($this->header_prefix.'Remaining: '.($this->ip_per_hour - $limitData['count'])); |
||
438 | header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch'])); |
||
439 | } |
||
440 | } else { |
||
441 | header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s')))); |
||
442 | $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits. |
||
443 | } |
||
444 | } |
||
445 | } |
||
446 | /** |
||
447 | * [custom_auth description] |
||
448 | * @param string $auth [description] |
||
449 | */ |
||
450 | private function custom_auth(string &$auth):void |
||
451 | { |
||
452 | // Header Check. |
||
453 | if (!isset($_SERVER[$auth])) { |
||
454 | $this->handle_response(RESTResponse::BAD_REQUEST, $auth); |
||
455 | } |
||
456 | // Callback Check. |
||
457 | if (!isset($this->config['auth_callbacks'][$auth])) { |
||
458 | $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits. |
||
459 | } |
||
460 | // Authentication. |
||
461 | if (!$this->config['auth_callbacks'][$auth]($this, $this->ci->security->xss_clean($_SERVER[$auth]))) { |
||
462 | $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits. |
||
463 | } |
||
464 | } |
||
465 | /** |
||
466 | * [get_authorization_header description] |
||
467 | * @return [type] [description] |
||
0 ignored issues
–
show
|
|||
468 | */ |
||
469 | private function get_authorization_header():?string |
||
470 | { |
||
471 | if (isset($_SERVER['Authorization'])) { |
||
472 | return trim($_SERVER["Authorization"]); |
||
473 | } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI |
||
474 | return trim($_SERVER["HTTP_AUTHORIZATION"]); |
||
475 | } elseif (function_exists('apache_request_headers')) { |
||
476 | $requestHeaders = apache_request_headers(); |
||
477 | |||
478 | // Avoid Surprises. |
||
479 | $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); |
||
480 | |||
481 | if (isset($requestHeaders['Authorization'])) { |
||
482 | return trim($requestHeaders['Authorization']); |
||
483 | } |
||
484 | } |
||
485 | return null; |
||
486 | } |
||
487 | |||
488 | /** |
||
489 | * [handle_response description] |
||
490 | * @param int $code [description] |
||
491 | */ |
||
492 | private function handle_response(int $code, $auth=null, ?string $errorReason=null):void |
||
493 | { |
||
494 | http_response_code($code); |
||
495 | header("Content-Type: application/json"); |
||
496 | if (isset($this->config['response_callbacks'][$code])) { |
||
497 | $this->config['response_callbacks'][$code]($auth, $errorReason); |
||
498 | } |
||
499 | if (ENVIRONMENT != 'testing') exit($code); |
||
0 ignored issues
–
show
|
|||
500 | throw new Exception("Error $code in $auth", $code); |
||
501 | } |
||
502 | } |
||
503 | ?> |
||
0 ignored issues
–
show
It is not recommended to use PHP's closing tag
?> in files other than templates.
Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore. A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.
Loading history...
|
|||
504 |