Completed
Push — master ( 9c2b09...5e7564 )
by Francis
01:33
created

libraries/REST.php (5 issues)

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
   * [public description]
79
   * @var [type]
80
   */
81
  public $allowedIPs;
82
  /**
83
   * [PACKAGE description]
84
   * @var string
85
   */
86
  const   PACKAGE    = "francis94c/ci-rest";
87
  /**
88
   * [RATE_LIMIT description]
89
   * @var string
90
   */
91
  const   RATE_LIMIT = "RateLimit";
92
93
  /**
94
   * [__construct This is the part of the code that takes care of all
95
   * authentiations. allowijg you to focus on build wonderfult things at REST.
96
   * pun intended ;-)]
97
   * @param array|null $params Initialization parameters from the Slint system.
98
   *                           There's no use for this arg yet.
99
   */
100
  function __construct($params=null) {
0 ignored issues
show
The parameter $params is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

100
  function __construct(/** @scrutinizer ignore-unused */ $params=null) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
101
    $this->ci =& get_instance();
0 ignored issues
show
The function get_instance was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

101
    $this->ci =& /** @scrutinizer ignore-call */ get_instance();
Loading history...
102
    // Load Config If Exists.
103
    $this->ci->config->load('rest', true, true);
104
    // Load Database.
105
    $this->ci->load->database();
106
    // load URL Helper
107
    $this->ci->load->helper('url');
108
    // Load REST Helper.
109
    $this->ci->load->splint(self::PACKAGE, '%rest');
110
    // Load Model.
111
    $this->ci->load->splint(self::PACKAGE, '*RESTModel', 'rest_model');
112
    $this->rest_model =& $this->ci->rest_model;
113
    $config = [
114
      'users_table'           => $this->ci->config->item('rest')['basic_auth']['users_table'] ?? null,
115
      'users_id_column'       => $this->ci->config->item('rest')['basic_auth']['id_column'] ?? null,
116
      'users_username_column' => $this->ci->config->item('rest')['basic_auth']['username_column'] ?? null,
117
      'users_email_column'    => $this->ci->config->item('rest')['basic_auth']['email_column'] ?? null,
118
      'users_password_column' => $this->ci->config->item('rest')['basic_auth']['password_column'] ?? null,
119
      'api_key_table'         => $this->ci->config->item('rest')['api_key_auth']['api_key_table'] ?? null,
120
      'api_key_column'        => $this->ci->config->item('rest')['api_key_auth']['api_key_column'] ?? null,
121
      'api_key_limit_column'  => $this->ci->config->item('rest')['api_key_auth']['api_key_limit_column'] ?? null
122
    ];
123
    $this->rest_model->init($config);
124
    // Load Variable(s) from Config.
125
    $this->allowedIPs = $this->ci->config->item('rest')['allowed_ips'] ?? ['127.0.0.1', '[::1]'];
126
    $this->apiKeyHeader = $this->ci->config->item('rest')['api_key_header'] ?? 'X-API-KEY';
127
    $this->api_key_limit_column = $this->ci->config->item('rest')['api_key_auth']['api_key_limit_column'] ?? null;
128
    $this->api_key_column = $this->ci->config->item('rest')['api_key_auth']['api_key_column'] ?? null;
129
    $this->limit_api = $this->ci->config->item('rest')['api_limiter']['api_limiter'] ?? false;
130
    $this->per_hour = $this->ci->config->item('rest')['api_limiter']['per_hour'] ?? 100;
131
    $this->ip_per_hour = $this->ci->config->item('rest')['api_limiter']['ip_per_hour'] ?? 50;
132
    $this->show_header = $this->ci->config->item('rest')['api_limiter']['show_header'] ?? null;
133
    $this->whitelist = $this->ci->config->item('rest')['api_limiter']['whitelist'] ?? null;
134
    $this->header_prefix = $this->ci->config->item('rest')['api_limiter']['header_prefix'] ?? 'X-RateLimit-';
135
    // Authenticate
136
    $this->authenticate();
137
138
    // Generic Rate Limiter.
139
    if ($this->limit_api && !$this->checked_rate_limit &&
140
    ($this->ci->config->item('rest')['api_limiter']['limit_by_ip'] ?? false)) {
141
      $this->api_rest_limit_by_ip_address();
142
    }
143
144
    log_message('debug', 'REST Request Authenticated and REST Library Initialized.');
145
  }
146
  /**
147
   * [authenticate description]
148
   */
149
  private function authenticate():void {
150
    $uri_auths = $this->ci->config->item('rest')['uri_auth'] ?? null;
151
    // Match Auth Routes.
152
    // The below algorithm is similar to the one Code Igniter uses in its
153
    // Routing Class.
154
    if ($uri_auths == null || !is_array($uri_auths)) return;
155
    $auths = null;
156
    foreach ($uri_auths as $uri => $auth_array) {
157
      // Convert wildcards to RegEx.
158
			$uri = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $uri);
159
      if (preg_match('#^'.$uri.'$#', uri_string())) $auths = $auth_array; // Assign Authentication Steps.
0 ignored issues
show
The function uri_string was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

159
      if (preg_match('#^'.$uri.'$#', /** @scrutinizer ignore-call */ uri_string())) $auths = $auth_array; // Assign Authentication Steps.
Loading history...
160
      break;
161
    }
162
    //$auths = $this->ci->config->item('rest')['uri_auth'][uri_string()] ?? null;
163
    if ($auths == null) return; // No authentication(s) to acrry out.
164
    // $this->process_auth() terminates the script if authentication fails
165
    // It will call the callable in the rest.php config file under
166
    // response_callbacks which matches the necesarry RESTResponse constant
167
    // before exiting. Which callable is called in any situation is documented
168
    // in README.md
169
    if (is_scalar($auths)) {
170
      $this->process_auth($auths);
171
      return;
172
    }
173
    foreach ($auths as $auth) $this->process_auth($auth);
174
  }
175
  /**
176
   * [process_auth description]
177
   * @param  string $auth [description]
178
   * @return bool         [description]
179
   */
180
  private function process_auth(string &$auth):void {
181
    switch ($auth) {
182
      case RESTAuth::IP: $this->ip_auth(); break;
183
      case RESTAuth::BASIC: $this->basic_auth(); break;
184
      case RESTAuth::API_KEY: $this->api_key_auth(); break;
185
      case RESTAuth::OAUTH2: $this->bearer_auth(RESTAuth::OAUTH2); break;
186
      case RESTAuth::BEARER: $this->bearer_auth(); break;
187
      default: $this->custom_auth($auth);
188
    }
189
  }
190
  /**
191
   * [ip_auth description]
192
   */
193
  private function ip_auth():void {
194
    if (!in_array($this->ci->input->ip_address(), $this->allowedIPs)) {
195
      $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::IP); // Exits.
196
    }
197
  }
198
  /**
199
   * [bearer_auth description]
200
   */
201
  private function bearer_auth($auth=RESTAuth::BEARER):void {
202
    $authorization = $this->get_authorization_header();
203
    if ($authorization == null || substr_count($authorization, " ") != 1) {
204
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth); // Exits.
205
    }
206
    $token = explode(" ", $authorization);
207
    if ($token[0] != "Bearer") {
208
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth); // Exits.
209
    }
210
    $this->token = $token[1];
211
    // Call Up Custom Implemented Bearer/Token Authorization.
212
    // Callback Check.
213
    if (!isset($this->ci->config->item('rest')['auth_callbacks'][$auth])) {
214
      $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits.
215
    }
216
    // Authorization.
217
    if (!$this->ci->config->item('rest')['auth_callbacks'][$auth]($this, $this->token)) {
218
      $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits.
219
    }
220
  }
221
  /**
222
   * [basic_auth description]
223
   */
224
  private function basic_auth():void {
225
    $username = $_SERVER['PHP_AUTH_USER'] ?? null;
226
    $password = $_SERVER['PHP_AUTH_PW'] ?? null;
227
    if (!$username || !$password) $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::BASIC); // Exits.
228
    if (!$this->rest_model->basicAuth($this, $username, $password)) $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::BASIC); // Exits.
229
  }
230
  /**
231
   * [api_key_auth description]
232
   */
233
  private function api_key_auth():void {
234
    if (!isset($_SERVER['HTTP_' . str_replace("-", "_", $this->apiKeyHeader)])) {
235
      $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::API_KEY); // Exits.
236
    }
237
    $apiKey = $this->rest_model->getAPIKeyData(
238
      $_SERVER['HTTP_' . str_replace("-", "_", $this->apiKeyHeader)]
239
    );
240
    if ($apiKey == null) {
241
      $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::API_KEY); // Exits.
242
    }
243
    // API KEY Auth Passed Above.
244
    if ($this->limit_api && $this->api_key_limit_column != null && $apiKey[$this->api_key_limit_column] == 1) {
245
      // Trunctate Rate Limit Data.
246
      $this->rest_model->truncateRatelimitData();
247
      // Check Whitelist.
248
      if (in_array($this->ci->input->ip_address(), $this->whitelist)) {
0 ignored issues
show
It seems like $this->whitelist can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

248
      if (in_array($this->ci->input->ip_address(), /** @scrutinizer ignore-type */ $this->whitelist)) {
Loading history...
249
        $this->checked_rate_limit = true; // Ignore Limit By IP.
250
        return;
251
      }
252
      // Should we acyually Limit?
253
      if ($this->per_hour > 0) {
254
        $client = hash('md5', $this->ci->input->ip_address() . "%" . $apiKey[$this->api_key_column]);
255
        $limitData = $this->rest_model->getLimitData($client, '_api_keyed_user');
256
        if ($limitData == null) {
257
          $limitData = [];
258
          $limitData['count'] = 0;
259
          $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60));
260
          $limitData['start'] = date('d M Y H:i:s');
261
        }
262
        if ($this->per_hour - $limitData['count'] > 0) {
263
          if (!$this->rest_model->insertLimitData($client, '_api_keyed_user')) {
264
            $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits.
265
          }
266
          ++$limitData['count'];
267
          if ($this->show_header) {
268
            header($this->header_prefix.'Limit: '.$this->per_hour);
269
            header($this->header_prefix.'Remaining: '.($this->per_hour - $limitData['count']));
270
            header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch']));
271
          }
272
        } else {
273
          header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s'))));
274
          $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits.
275
        }
276
      }
277
    }
278
    $this->checked_rate_limit = true; // Ignore Limit By IP.
279
  }
280
  /**
281
   * [api_rest_limit_by_ip_address description]
282
   * TODO: Implement.
283
   */
284
  private function api_rest_limit_by_ip_address():void {
285
    // Trunctate Rate Limit Data.
286
    $this->rest_model->truncateRatelimitData();
287
    // Check Whitelist.
288
    if (in_array($this->ci->input->ip_address(), $this->whitelist)) return;
0 ignored issues
show
It seems like $this->whitelist can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

288
    if (in_array($this->ci->input->ip_address(), /** @scrutinizer ignore-type */ $this->whitelist)) return;
Loading history...
289
    // Should we acyually Limit?
290
    if ($this->ip_per_hour > 0) {
291
      $client = hash('md5', $this->ci->input->ip_address());
292
      $limitData = $this->rest_model->getLimitData($client, '_ip_address');
293
      if ($limitData == null) {
294
        $limitData = [];
295
        $limitData['count'] = 0;
296
        $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60));
297
        $limitData['start'] = date('d M Y H:i:s');
298
      }
299
      if ($this->ip_per_hour - $limitData['count'] > 0) {
300
        if (!$this->rest_model->insertLimitData($client, '_ip_address')) {
301
          $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits.
302
        }
303
        ++$limitData['count'];
304
        if ($this->show_header) {
305
          header($this->header_prefix.'Limit: '.$this->ip_per_hour);
306
          header($this->header_prefix.'Remaining: '.($this->ip_per_hour - $limitData['count']));
307
          header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch']));
308
        }
309
      } else {
310
        header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s'))));
311
        $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits.
312
      }
313
    }
314
  }
315
  /**
316
   * [custom_auth description]
317
   * @param string $auth [description]
318
   */
319
  private function custom_auth(string &$auth):void {
320
    // Header Check.
321
    if (!isset($_SERVER[$auth])) {
322
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth);
323
    }
324
    // Callback Check.
325
    if (!isset($this->ci->config->item('rest')['auth_callbacks'][$auth])) {
326
      $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits.
327
    }
328
    // Authentication.
329
    if (!$this->ci->config->item('rest')['auth_callbacks'][$auth]($this, $this->ci->security->xss_clean($_SERVER[$auth]))) {
330
      $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits.
331
    }
332
  }
333
  /**
334
   * [get_authorization_header description]
335
   * @return [type] [description]
336
   */
337
  private function get_authorization_header():?string {
338
    if (isset($_SERVER['Authorization'])) {
339
      return trim($_SERVER["Authorization"]);
340
    } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI
341
      return trim($_SERVER["HTTP_AUTHORIZATION"]);
342
    } elseif (function_exists('apache_request_headers')) {
343
      $requestHeaders = apache_request_headers();
344
345
      // Avoid Surprises.
346
      $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
347
348
      if (isset($requestHeaders['Authorization'])) {
349
        return trim($requestHeaders['Authorization']);
350
      }
351
    }
352
    return null;
353
  }
354
  /**
355
   * [handle_response description]
356
   * @param int $code [description]
357
   */
358
  private function handle_response(int $code, $auth=null):void {
359
    http_response_code($code);
360
    header("Content-Type: application/json");
361
    if (isset($this->ci->config->item('rest')['response_callbacks'][$code])) {
362
      $this->ci->config->item('rest')['response_callbacks'][$code]($auth);
363
    }
364
    if (ENVIRONMENT != 'testing') exit($code);
365
    throw new Exception("Error $code in $auth", $code);
366
  }
367
}
368
?>
369