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] |
||||
14 | */ |
||||
15 | private $ci; |
||||
16 | |||||
17 | /** |
||||
18 | * [private description] |
||||
19 | * @var [type] |
||||
20 | */ |
||||
21 | private $api_key_limit_column; |
||||
22 | |||||
23 | /** |
||||
24 | * [private description] |
||||
25 | * @var [type] |
||||
26 | */ |
||||
27 | private $api_key_column; |
||||
28 | |||||
29 | /** |
||||
30 | * [private description] |
||||
31 | * @var [type] |
||||
32 | */ |
||||
33 | private $per_hour; |
||||
34 | |||||
35 | /** |
||||
36 | * [private description] |
||||
37 | * @var [type] |
||||
38 | */ |
||||
39 | private $ip_per_hour; |
||||
40 | |||||
41 | /** |
||||
42 | * [private description] |
||||
43 | * @var [type] |
||||
44 | */ |
||||
45 | private $show_header; |
||||
46 | |||||
47 | /** |
||||
48 | * [private description] |
||||
49 | * @var [type] |
||||
50 | */ |
||||
51 | private $whitelist; |
||||
52 | |||||
53 | /** |
||||
54 | * [private description] |
||||
55 | * @var [type] |
||||
56 | */ |
||||
57 | private $checked_rate_limit = false; |
||||
58 | |||||
59 | /** |
||||
60 | * [private description] |
||||
61 | * @var [type] |
||||
62 | */ |
||||
63 | private $header_prefix; |
||||
64 | |||||
65 | /** |
||||
66 | * [private description] |
||||
67 | * @var [type] |
||||
68 | */ |
||||
69 | private $limit_api; |
||||
70 | |||||
71 | /** |
||||
72 | * [public description] |
||||
73 | * @var [type] |
||||
74 | */ |
||||
75 | public $userId; |
||||
76 | |||||
77 | /** |
||||
78 | * [public description] |
||||
79 | * @var [type] |
||||
80 | */ |
||||
81 | public $apiKey; |
||||
82 | |||||
83 | /** |
||||
84 | * [public description] |
||||
85 | * @var [type] |
||||
86 | */ |
||||
87 | public $apiKeyHeader; |
||||
88 | |||||
89 | /** |
||||
90 | * [public description] |
||||
91 | * @var [type] |
||||
92 | */ |
||||
93 | public $token; |
||||
94 | |||||
95 | /** |
||||
96 | * [public description] |
||||
97 | * @var [type] |
||||
98 | */ |
||||
99 | public $allowedIps; |
||||
100 | |||||
101 | /** |
||||
102 | * [public description] |
||||
103 | * @var [type] |
||||
104 | */ |
||||
105 | public $config; |
||||
106 | |||||
107 | /** |
||||
108 | * [public description] |
||||
109 | * @var [type] |
||||
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; |
||||
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; |
||||
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; |
||||
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||||
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); |
||||
0 ignored issues
–
show
The call to
REST::custom_auth() has too many arguments starting with $flags .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.
Loading history...
|
|||||
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 | } |
||||
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] |
||||
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); |
||||
500 | throw new Exception("Error $code in $auth", $code); |
||||
501 | } |
||||
502 | } |
||||
503 | ?> |
||||
504 |