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 | * [private description] |
||
18 | * @var [type] |
||
19 | */ |
||
20 | private $api_key_limit_column; |
||
21 | /** |
||
22 | * [private description] |
||
23 | * @var [type] |
||
24 | */ |
||
25 | private $api_key_column; |
||
26 | /** |
||
27 | * [private description] |
||
28 | * @var [type] |
||
29 | */ |
||
30 | private $per_hour; |
||
31 | /** |
||
32 | * [private description] |
||
33 | * @var [type] |
||
34 | */ |
||
35 | private $ip_per_hour; |
||
36 | /** |
||
37 | * [private description] |
||
38 | * @var [type] |
||
39 | */ |
||
40 | private $show_header; |
||
41 | /** |
||
42 | * [private description] |
||
43 | * @var [type] |
||
44 | */ |
||
45 | private $whitelist; |
||
46 | /** |
||
47 | * [private description] |
||
48 | * @var [type] |
||
49 | */ |
||
50 | private $checked_rate_limit = false; |
||
51 | /** |
||
52 | * [private description] |
||
53 | * @var [type] |
||
54 | */ |
||
55 | private $header_prefix; |
||
56 | /** |
||
57 | * [private description] |
||
58 | * @var [type] |
||
59 | */ |
||
60 | private $limit_api; |
||
61 | |||
62 | /** |
||
63 | * [public description] |
||
64 | * @var [type] |
||
65 | */ |
||
66 | public $userId; |
||
67 | /** |
||
68 | * [public description] |
||
69 | * @var [type] |
||
70 | */ |
||
71 | public $apiKeyHeader; |
||
72 | /** |
||
73 | * [public description] |
||
74 | * @var [type] |
||
75 | */ |
||
76 | public $token; |
||
77 | /** |
||
78 | * [PACKAGE description] |
||
79 | * @var string |
||
80 | */ |
||
81 | const PACKAGE = "francis94c/ci-rest"; |
||
82 | /** |
||
83 | * [RATE_LIMIT description] |
||
84 | * @var string |
||
85 | */ |
||
86 | const RATE_LIMIT = "RateLimit"; |
||
87 | |||
88 | /** |
||
89 | * [__construct description] |
||
90 | * @param [type] $params [description] |
||
91 | */ |
||
92 | function __construct($params=null) { |
||
93 | $this->ci =& get_instance(); |
||
94 | // Load Config If Exists. |
||
95 | $this->ci->config->load('rest', true, true); |
||
96 | // Load Database. |
||
97 | $this->ci->load->database(); |
||
98 | // Load Model. |
||
99 | $this->ci->load->splint(self::PACKAGE, '*RESTModel', 'rest_model'); |
||
100 | $this->rest_model =& $this->ci->rest_model; |
||
101 | $config = [ |
||
102 | 'users_table' => $this->ci->config->item('rest')['basic_auth']['users_table'] ?? null, |
||
103 | 'users_id_column' => $this->ci->config->item('rest')['basic_auth']['id_column'] ?? null, |
||
104 | 'users_username_column' => $this->ci->config->item('rest')['basic_auth']['username_column'] ?? null, |
||
105 | 'users_email_column' => $this->ci->config->item('rest')['basic_auth']['email_column'] ?? null, |
||
106 | 'users_password_column' => $this->ci->config->item('rest')['basic_auth']['password_column'] ?? null, |
||
107 | 'api_key_table' => $this->ci->config->item('rest')['api_key_auth']['api_key_table'] ?? null, |
||
108 | 'api_key_column' => $this->ci->config->item('rest')['api_key_auth']['api_key_column'] ?? null, |
||
109 | 'api_key_limit_column' => $this->ci->config->item('rest')['api_key_auth']['api_key_limit_column'] ?? null |
||
110 | ]; |
||
111 | $this->rest_model->init($config); |
||
112 | // Load Variable(s) from Config. |
||
113 | $this->apiKeyHeader = $this->ci->config->item('rest')['api_key_header'] ?? 'X-API-KEY'; |
||
114 | $this->api_key_limit_column = $this->ci->config->item('rest')['api_key_auth']['api_key_limit_column'] ?? null; |
||
115 | $this->api_key_column = $this->ci->config->item('rest')['api_key_auth']['api_key_column'] ?? null; |
||
116 | $this->limit_api = $this->ci->config->item('rest')['api_limiter']['api_limiter'] ?? false; |
||
117 | $this->per_hour = $this->ci->config->item('rest')['api_limiter']['per_hour'] ?? 100; |
||
118 | $this->ip_per_hour = $this->ci->config->item('rest')['api_limiter']['ip_per_hour'] ?? 50; |
||
119 | $this->show_header = $this->ci->config->item('rest')['api_limiter']['show_header'] ?? null; |
||
120 | $this->whitelist = $this->ci->config->item('rest')['api_limiter']['whitelist'] ?? null; |
||
121 | $this->header_prefix = $this->ci->config->item('rest')['api_limiter']['header_prefix'] ?? 'X-RateLimit-'; |
||
122 | // Authenticate |
||
123 | $this->authenticate(); |
||
124 | |||
125 | // Generic Rate Limiter. |
||
126 | if ($this->limit_api && !$this->checked_rate_limit && |
||
127 | ($this->ci->config->item('rest')['api_limiter']['limit_by_ip'] ?? false)) { |
||
128 | $this->api_rest_limit_by_ip_address(); |
||
129 | } |
||
130 | |||
131 | log_message('debug', 'REST Request Authenticated and REST Library Initialized.'); |
||
132 | } |
||
133 | /** |
||
134 | * [authenticate description] |
||
135 | */ |
||
136 | private function authenticate():void { |
||
137 | $uri_auths = $this->ci->config->item('rest')['uri_auth'] ?? null; |
||
138 | // Match Auth Routes. |
||
139 | // The below algorithm is similar to the one Code Igniter uses in its |
||
140 | // Routing Class. |
||
141 | if ($uri_auths == null || !is_array($uri_auths)) return; |
||
142 | $auths = null; |
||
143 | foreach ($uri_auths as $uri => $auth_array) { |
||
144 | // Convert wildcards to RegEx. |
||
145 | $uri = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $uri); |
||
146 | if (preg_match('#^'.$uri.'$#', uri_string())) $auths = $auth_array; // Assign Authentication Steps. |
||
147 | break; |
||
148 | } |
||
149 | //$auths = $this->ci->config->item('rest')['uri_auth'][uri_string()] ?? null; |
||
150 | if ($auths == null) return; // No authentication(s) to acrry out. |
||
151 | // $this->process_auth() terminates the script if authentication fails |
||
152 | // It will call the callable in the rest.php config file under |
||
153 | // response_callbacks which matches the necesarry RESTResponse constant |
||
154 | // before exiting. Which callable is called in any situation is documented |
||
155 | // in README.md |
||
156 | if (is_scalar($auths)) { |
||
157 | $this->process_auth($auths); |
||
158 | return; |
||
159 | } |
||
160 | foreach ($auths as $auth) $this->process_auth($auth); |
||
161 | } |
||
162 | /** |
||
163 | * [process_auth description] |
||
164 | * @param string $auth [description] |
||
165 | * @return bool [description] |
||
166 | */ |
||
167 | private function process_auth(string &$auth):void { |
||
168 | switch ($auth) { |
||
169 | case RESTAuth::BASIC: $this->basic_auth(); break; |
||
170 | case RESTAuth::API_KEY: $this->api_key_auth(); break; |
||
171 | case RESTAuth::OAUTH2: $this->bearer_auth(RESTAuth::OAUTH2); break; |
||
172 | case RESTAuth::BEARER: $this->bearer_auth(); break; |
||
173 | default: $this->custom_auth($auth); |
||
174 | } |
||
175 | } |
||
176 | /** |
||
177 | * [bearer_auth description] |
||
178 | */ |
||
179 | private function bearer_auth($auth=RESTAuth::BEARER):void { |
||
180 | $authorization = $this->get_authorization_header(); |
||
181 | if ($authorization == null || substr_count($authorization, " ") != 1) { |
||
182 | $this->handle_response(RESTResponse::BAD_REQUEST, $auth); // Exits. |
||
183 | } |
||
184 | $token = explode(" ", $authorization); |
||
185 | if ($token[0] != "Bearer") { |
||
186 | $this->handle_response(RESTResponse::BAD_REQUEST, $auth); // Exits. |
||
187 | } |
||
188 | $this->token = $token[1]; |
||
189 | // Call Up Custom Implemented Bearer/Token Authorization. |
||
190 | // Callback Check. |
||
191 | if (!isset($this->ci->config->item('rest')['auth_callbacks'][$auth])) { |
||
192 | $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits. |
||
193 | } |
||
194 | // Authorization. |
||
195 | if (!$this->ci->config->item('rest')['auth_callbacks'][$auth]($this, $this->token)) { |
||
196 | $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits. |
||
197 | } |
||
198 | } |
||
199 | /** |
||
200 | * [basic_auth description] |
||
201 | */ |
||
202 | private function basic_auth():void { |
||
203 | $username = $_SERVER['PHP_AUTH_USER'] ?? null; |
||
204 | $password = $_SERVER['PHP_AUTH_PW'] ?? null; |
||
205 | if (!$username || !$password) $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::BASIC); // Exits. |
||
206 | if (!$this->rest_model->basicAuth($this, $username, $password)) $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::BASIC); // Exits. |
||
207 | } |
||
208 | /** |
||
209 | * [api_key_auth description] |
||
210 | */ |
||
211 | private function api_key_auth():void { |
||
212 | if (!isset($_SERVER['HTTP_' . str_replace("-", "_", $this->apiKeyHeader)])) { |
||
213 | $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::API_KEY); // Exits. |
||
214 | } |
||
215 | $apiKey = $this->rest_model->getAPIKeyData( |
||
216 | $_SERVER['HTTP_' . str_replace("-", "_", $this->apiKeyHeader)] |
||
217 | ); |
||
218 | if ($apiKey == null) { |
||
219 | $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::API_KEY); // Exits. |
||
220 | } |
||
221 | // API KEY Auth Passed Above. |
||
222 | if ($this->limit_api && $this->api_key_limit_column != null && $apiKey[$this->api_key_limit_column] == 1) { |
||
223 | // Trunctate Rate Limit Data. |
||
224 | $this->rest_model->truncateRatelimitData(); |
||
225 | // Check Whitelist. |
||
226 | if (in_array($this->ci->input->ip_address(), $this->whitelist)) { |
||
227 | $this->checked_rate_limit = true; // Ignore Limit By IP. |
||
228 | return; |
||
229 | } |
||
230 | // Should we acyually Limit? |
||
231 | if ($this->per_hour > 0) { |
||
232 | $client = hash('md5', $this->ci->input->ip_address() . "%" . $apiKey[$this->api_key_column]); |
||
233 | $limitData = $this->rest_model->getLimitData($client, '_api_keyed_user'); |
||
234 | if ($limitData == null) { |
||
235 | $limitData = []; |
||
236 | $limitData['count'] = 0; |
||
237 | $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60)); |
||
238 | $limitData['start'] = date('d M Y H:i:s'); |
||
239 | } |
||
240 | if ($this->per_hour - $limitData['count'] > 0) { |
||
241 | if (!$this->rest_model->insertLimitData($client, '_api_keyed_user')) { |
||
242 | $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits. |
||
243 | } |
||
244 | ++$limitData['count']; |
||
245 | if ($this->show_header) { |
||
246 | header($this->header_prefix.'Limit: '.$this->per_hour); |
||
247 | header($this->header_prefix.'Remaining: '.($this->per_hour - $limitData['count'])); |
||
248 | header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch'])); |
||
249 | } |
||
250 | } else { |
||
251 | header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s')))); |
||
252 | $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits. |
||
253 | } |
||
254 | } |
||
255 | } |
||
256 | $this->checked_rate_limit = true; // Ignore Limit By IP. |
||
257 | } |
||
258 | /** |
||
259 | * [api_rest_limit_by_ip_address description] |
||
260 | * TODO: Implement. |
||
261 | */ |
||
262 | private function api_rest_limit_by_ip_address():void { |
||
263 | // Trunctate Rate Limit Data. |
||
264 | $this->rest_model->truncateRatelimitData(); |
||
265 | // Check Whitelist. |
||
266 | if (in_array($this->ci->input->ip_address(), $this->whitelist)) return; |
||
267 | // Should we acyually Limit? |
||
268 | if ($this->ip_per_hour > 0) { |
||
269 | $client = hash('md5', $this->ci->input->ip_address()); |
||
270 | $limitData = $this->rest_model->getLimitData($client, '_ip_address'); |
||
271 | if ($limitData == null) { |
||
272 | $limitData = []; |
||
273 | $limitData['count'] = 0; |
||
274 | $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60)); |
||
275 | $limitData['start'] = date('d M Y H:i:s'); |
||
276 | } |
||
277 | if ($this->ip_per_hour - $limitData['count'] > 0) { |
||
278 | if (!$this->rest_model->insertLimitData($client, '_ip_address')) { |
||
279 | $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits. |
||
280 | } |
||
281 | ++$limitData['count']; |
||
282 | if ($this->show_header) { |
||
283 | header($this->header_prefix.'Limit: '.$this->ip_per_hour); |
||
284 | header($this->header_prefix.'Remaining: '.($this->ip_per_hour - $limitData['count'])); |
||
285 | header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch'])); |
||
286 | } |
||
287 | } else { |
||
288 | header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s')))); |
||
289 | $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits. |
||
290 | } |
||
291 | } |
||
292 | } |
||
293 | /** |
||
294 | * [custom_auth description] |
||
295 | * @param string $auth [description] |
||
296 | */ |
||
297 | private function custom_auth(string &$auth):void { |
||
298 | // Header Check. |
||
299 | if (!isset($_SERVER[$auth])) { |
||
300 | $this->handle_response(RESTResponse::BAD_REQUEST, $auth); |
||
301 | } |
||
302 | // Callback Check. |
||
303 | if (!isset($this->ci->config->item('rest')['auth_callbacks'][$auth])) { |
||
304 | $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits. |
||
305 | } |
||
306 | // Authentication. |
||
307 | if (!$this->ci->config->item('rest')['auth_callbacks'][$auth]($this, $this->ci->security->xss_clean($_SERVER[$auth]))) { |
||
308 | $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits. |
||
309 | } |
||
310 | } |
||
311 | /** |
||
312 | * [get_authorization_header description] |
||
313 | * @return [type] [description] |
||
314 | */ |
||
315 | private function get_authorization_header():?string { |
||
316 | if (isset($_SERVER['Authorization'])) { |
||
317 | return trim($_SERVER["Authorization"]); |
||
318 | } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI |
||
319 | return trim($_SERVER["HTTP_AUTHORIZATION"]); |
||
320 | } elseif (function_exists('apache_request_headers')) { |
||
321 | $requestHeaders = apache_request_headers(); |
||
322 | |||
323 | // Avoid Surprises. |
||
324 | $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); |
||
325 | |||
326 | if (isset($requestHeaders['Authorization'])) { |
||
327 | return trim($requestHeaders['Authorization']); |
||
328 | } |
||
329 | } |
||
330 | return null; |
||
331 | } |
||
332 | /** |
||
333 | * [handle_response description] |
||
334 | * @param int $code [description] |
||
335 | */ |
||
336 | private function handle_response(int $code, $auth=null):void { |
||
337 | http_response_code($code); |
||
338 | header("Content-Type: application/json"); |
||
339 | if (isset($this->ci->config->item('rest')['response_callbacks'][$code])) { |
||
340 | $this->ci->config->item('rest')['response_callbacks'][$code]($auth); |
||
341 | } |
||
342 | if (ENVIRONMENT != 'testing') exit($code); |
||
0 ignored issues
–
show
|
|||
343 | throw new Exception("Error $code in $auth", $code); |
||
344 | } |
||
345 | } |
||
346 | ?> |
||
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...
|
|||
347 |
In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.