Passed
Push — master ( 5686c5...33dcd4 )
by Francis
01:27
created

REST::process_auth()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 8
c 1
b 0
f 0
nc 7
nop 1
dl 0
loc 9
rs 8.8333
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
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
14
   */
15
  private $ci;
16
17
  /**
18
   * [private description]
19
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
20
   */
21
  private $api_key_limit_column;
22
23
  /**
24
   * [private description]
25
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
26
   */
27
  private $api_key_column;
28
29
  /**
30
   * [private description]
31
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
32
   */
33
  private $per_hour;
34
35
  /**
36
   * [private description]
37
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
38
   */
39
  private $ip_per_hour;
40
41
  /**
42
   * [private description]
43
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
44
   */
45
  private $show_header;
46
47
  /**
48
   * [private description]
49
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
50
   */
51
  private $whitelist;
52
53
  /**
54
   * [private description]
55
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
56
   */
57
  private $checked_rate_limit = false;
58
59
  /**
60
   * [private description]
61
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
62
   */
63
  private $header_prefix;
64
65
  /**
66
   * [private description]
67
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
68
   */
69
  private $limit_api;
70
71
  /**
72
   * [public description]
73
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
74
   */
75
  public  $userId;
76
77
  /**
78
   * [public description]
79
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
80
   */
81
  public $apiKey;
82
83
  /**
84
   * [public description]
85
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
86
   */
87
  public  $apiKeyHeader;
88
89
  /**
90
   * [public description]
91
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
92
   */
93
  public  $token;
94
95
  /**
96
   * [public description]
97
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
98
   */
99
  public $allowedIps;
100
101
  /**
102
   * [public description]
103
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
104
   */
105
  public $config;
106
107
  /**
108
   * [PACKAGE description]
109
   * @var string
110
   */
111
  const PACKAGE = "francis94c/ci-rest";
112
113
  /**
114
   * [RATE_LIMIT description]
115
   * @var string
116
   */
117
  const RATE_LIMIT = "RateLimit";
118
119
  /**
120
   * [__construct This is the part of the code that takes care of all
121
   * authentiations. allowing you to focus on building wonderful things at REST.
122
   * pun intended ;-)]
123
   * @param array|null $params Initialization parameters from the Slint system.
124
   *                           There's no use for this arg yet.
125
   */
126
  function __construct(?array $params=null)
127
  {
128
    $this->ci =& get_instance();
0 ignored issues
show
Bug introduced by
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

128
    $this->ci =& /** @scrutinizer ignore-call */ get_instance();
Loading history...
129
130
    if ($this->ci->input->is_cli_request()) return;
131
132
    // Load Config If Exists.
133
    //$this->ci->config->load('rest', true, true);
134
    if (is_file(APPPATH . 'config/rest.php')) {
0 ignored issues
show
Bug introduced by
The constant APPPATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
135
      include APPPATH . 'config/rest.php';
136
    }
137
138
    $this->config = $config;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $config seems to be never defined.
Loading history...
139
140
    // Load Database.
141
    $this->ci->load->database();
142
143
    // load URL Helper
144
    $this->ci->load->helper('url');
145
146
    // Load REST Helper.
147
    $this->ci->load->splint(self::PACKAGE, '%rest');
148
149
    // Load Model.
150
    $this->ci->load->splint(self::PACKAGE, '*RESTModel', 'rest_model');
151
    $this->rest_model =& $this->ci->rest_model;
0 ignored issues
show
Bug Best Practice introduced by
The property rest_model does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
152
153
    $this->rest_model->init([
154
      'users_table'           => $config['basic_auth']['users_table'] ?? null,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $config seems to never exist and therefore isset should always be false.
Loading history...
155
      'users_id_column'       => $config['basic_auth']['id_column'] ?? null,
156
      'users_username_column' => $config['basic_auth']['username_column'] ?? null,
157
      'users_email_column'    => $config['basic_auth']['email_column'] ?? null,
158
      'users_password_column' => $config['basic_auth']['password_column'] ?? null,
159
      'api_key_table'         => $config['api_key_auth']['api_key_table'] ?? null,
160
      'api_key_column'        => $config['api_key_auth']['api_key_column'] ?? null,
161
      'api_key_limit_column'  => $config['api_key_auth']['api_key_limit_column'] ?? null
162
    ]);
163
164
    // Load Variable(s) from Config.
165
    $this->allowedIps = $config['allowed_ips'] ?? ['127.0.0.1', '[::1]'];
166
    $this->apiKeyHeader = $config['api_key_header'] ?? 'X-API-KEY';
167
    $this->api_key_limit_column = $config['api_key_auth']['api_key_limit_column'] ?? null;
168
    $this->api_key_column = $config['api_key_auth']['api_key_column'] ?? null;
169
    $this->limit_api = $config['api_limiter']['api_limiter'] ?? false;
170
    $this->per_hour = $config['api_limiter']['per_hour'] ?? 100;
171
    $this->ip_per_hour = $config['api_limiter']['ip_per_hour'] ?? 50;
172
    $this->show_header = $config['api_limiter']['show_header'] ?? null;
173
    $this->whitelist = $config['api_limiter']['whitelist'] ?? [];
174
    $this->header_prefix = $config['api_limiter']['header_prefix'] ?? 'X-RateLimit-';
175
176
    // Limit Only?
177
    //if ($this->config['api_limiter']['api_limit_only'] ?? false) {
178
      //return;
179
    //}
180
181
    // Authenticate
182
    $this->authenticate();
183
184
    // Generic Rate Limiter.
185
    if ($this->limit_api && !$this->checked_rate_limit &&
186
    ($config['api_limiter']['limit_by_ip'] ?? false)) {
187
      $this->api_rest_limit_by_ip_address();
188
    }
189
190
    log_message('debug', 'REST Request Authenticated and REST Library Initialized.');
0 ignored issues
show
Bug introduced by
The function log_message 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

190
    /** @scrutinizer ignore-call */ 
191
    log_message('debug', 'REST Request Authenticated and REST Library Initialized.');
Loading history...
191
  }
192
193
  /**
194
   * [authenticate description]
195
   * @date 2020-01-30
196
   */
197
  private function authenticate():void
198
  {
199
    $auths = null;
200
201
    $globalAuths = $this->config['global_auth'] ?? null;
202
203
    if ($globalAuths) $auths = is_array($globalAuths) ? $globalAuths : [$globalAuths];
204
205
    $uri_auths = $this->config['uri_auth'] ?? null;
206
207
    // Match Auth Routes.
208
    // The below algorithm is similar to the one Code Igniter uses in its
209
    // Routing Class.
210
    if ($uri_auths != null || is_array($uri_auths)) {
211
      foreach ($uri_auths as $uri => $auth_array) {
212
        // Convert wildcards to RegEx.
213
  			$uri = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $uri);
214
        if (preg_match('#^'.$uri.'$#', uri_string())) {
0 ignored issues
show
Bug introduced by
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

214
        if (preg_match('#^'.$uri.'$#', /** @scrutinizer ignore-call */ uri_string())) {
Loading history...
215
          // Assign Authentication Steps.
216
          if (is_array($auth_array)) {
217
            foreach ($auth_array as $auth) {
218
              $auths[] = $auth;
219
            }
220
          } else {
221
            $auths[] = $auth_array;
222
          }
223
        }
224
        break;
225
      }
226
    }
227
228
    //$auths = $this->config['uri_auth'][uri_string()] ?? null;
229
    if (!$auths) return; // No authentication(s) to carry out.
230
231
    // $this->process_auth() terminates the script if authentication fails
232
    // It will call the callable in the rest.php config file under
233
    // response_callbacks which matches the necesarry RESTResponse constant
234
    // before exiting. Which callable is called in any situation is documented
235
    // in README.md
236
    //if (is_scalar($auths)) {
237
      //$this->process_auth($auths);
238
      //return;
239
    //}
240
241
    foreach ($auths as $auth) $this->process_auth($auth);
242
  }
243
  /**
244
   * [process_auth description]
245
   * @param  string $auth [description]
246
   * @return bool         [description]
247
   */
248
  private function process_auth(string &$auth):void {
249
    switch ($auth) {
250
      case RESTAuth::IP: $this->ip_auth(); break;
251
      case RESTAuth::BASIC: $this->basic_auth(); break;
252
      case RESTAuth::API_KEY: $this->api_key_auth(); break;
253
      case RESTAuth::OAUTH2: $this->bearer_auth(RESTAuth::OAUTH2); break;
254
      case RESTAuth::BEARER: $this->bearer_auth(); break;
255
      case RESTAuth::SECRET: $this->bearer_auth(RESTAuth::SECRET); break;
256
      default: $this->custom_auth($auth);
257
    }
258
  }
259
  /**
260
   * [ip_auth description]
261
   */
262
  private function ip_auth():void {
263
    if (!in_array($this->ci->input->ip_address(), $this->allowedIps)) {
264
      $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::IP); // Exits.
265
    }
266
  }
267
  /**
268
   * [bearer_auth description]
269
   */
270
  private function bearer_auth($auth=RESTAuth::BEARER):void
271
  {
272
    $authorization = $this->get_authorization_header();
273
    if ($authorization == null || substr_count($authorization, ' ') != 1) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $authorization of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
274
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth, 'Bad Request'); // Exits.
275
    }
276
    $token = explode(" ", $authorization);
277
    if ($token[0] != $auth) {
278
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth, 'Invalid Authorization'); // Exits.
279
    }
280
    $this->token = $token[1];
281
    // Call Up Custom Implemented Bearer/Token Authorization.
282
    // Callback Check.
283
    if (!isset($this->config['auth_callbacks'][$auth])) {
284
      $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits.
285
    }
286
    // Authorization.
287
    if (!$this->config['auth_callbacks'][$auth]($this, $this->token)) {
288
      $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits.
289
    }
290
  }
291
  /**
292
   * [basic_auth description]
293
   */
294
  private function basic_auth():void {
295
    $username = $_SERVER['PHP_AUTH_USER'] ?? null;
296
    $password = $_SERVER['PHP_AUTH_PW'] ?? null;
297
    if (!$username || !$password) $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::BASIC); // Exits.
298
    if (!$this->rest_model->basicAuth($this, $username, $password)) $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::BASIC); // Exits.
299
  }
300
  /**
301
   * [api_key_auth description]
302
   */
303
  private function api_key_auth():void
304
  {
305
    if (uri_string() == '')  return;
0 ignored issues
show
Bug introduced by
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

305
    if (/** @scrutinizer ignore-call */ uri_string() == '')  return;
Loading history...
306
307
    if (!$this->ci->input->get_request_header($this->apiKeyHeader, true)) {
308
    // if (!isset($_SERVER['HTTP_' . str_replace("-", "_", $this->apiKeyHeader)])) {
309
      $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::API_KEY); // Exits.
310
    }
311
312
    $apiKey = $this->rest_model->getAPIKeyData(
313
      $this->ci->input->get_request_header($this->apiKeyHeader, true)
314
    );
315
316
    if ($apiKey == null) {
317
      $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::API_KEY); // Exits.
318
    }
319
320
    $this->apiKey = $apiKey;
321
322
    // API KEY Auth Passed Above.
323
    if ($this->limit_api && $this->api_key_limit_column != null && $apiKey->{$this->api_key_limit_column} == 1) {
324
      // Trunctate Rate Limit Data.
325
      $this->rest_model->truncateRatelimitData();
326
      // Check Whitelist.
327
      if (in_array($this->ci->input->ip_address(), $this->whitelist)) {
328
        $this->checked_rate_limit = true; // Ignore Limit By IP.
329
        return;
330
      }
331
      // Should we acyually Limit?
332
      if ($this->per_hour > 0) {
333
        $client = hash('md5', $this->ci->input->ip_address() . "%" . $apiKey->{$this->api_key_column});
334
        $limitData = $this->rest_model->getLimitData($client, '_api_keyed_user');
335
        if ($limitData == null) {
336
          $limitData = [];
337
          $limitData['count'] = 0;
338
          $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60));
339
          $limitData['start'] = date('d M Y H:i:s');
340
        }
341
        if ($this->per_hour - $limitData['count'] > 0) {
342
          if (!$this->rest_model->insertLimitData($client, '_api_keyed_user')) {
343
            $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits.
344
          }
345
          ++$limitData['count'];
346
          if ($this->show_header) {
347
            header($this->header_prefix.'Limit: '.$this->per_hour);
348
            header($this->header_prefix.'Remaining: '.($this->per_hour - $limitData['count']));
349
            header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch']));
350
          }
351
        } else {
352
          header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s'))));
353
          $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits.
354
        }
355
      }
356
    }
357
    $this->checked_rate_limit = true; // Ignore Limit By IP.
358
  }
359
  /**
360
   * [api_rest_limit_by_ip_address description]
361
   * TODO: Implement.
362
   */
363
  private function api_rest_limit_by_ip_address():void {
364
    // Trunctate Rate Limit Data.
365
    $this->rest_model->truncateRatelimitData();
366
    // Check Whitelist.
367
    if (in_array($this->ci->input->ip_address(), $this->whitelist)) return;
368
    // Should we acyually Limit?
369
    if ($this->ip_per_hour > 0) {
370
      $client = hash('md5', $this->ci->input->ip_address());
371
      $limitData = $this->rest_model->getLimitData($client, '_ip_address');
372
      if ($limitData == null) {
373
        $limitData = [];
374
        $limitData['count'] = 0;
375
        $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60));
376
        $limitData['start'] = date('d M Y H:i:s');
377
      }
378
      if ($this->ip_per_hour - $limitData['count'] > 0) {
379
        if (!$this->rest_model->insertLimitData($client, '_ip_address')) {
380
          $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits.
381
        }
382
        ++$limitData['count'];
383
        if ($this->show_header) {
384
          header($this->header_prefix.'Limit: '.$this->ip_per_hour);
385
          header($this->header_prefix.'Remaining: '.($this->ip_per_hour - $limitData['count']));
386
          header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch']));
387
        }
388
      } else {
389
        header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s'))));
390
        $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits.
391
      }
392
    }
393
  }
394
  /**
395
   * [custom_auth description]
396
   * @param string $auth [description]
397
   */
398
  private function custom_auth(string &$auth):void
399
  {
400
    // Header Check.
401
    if (!isset($_SERVER[$auth])) {
402
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth);
403
    }
404
    // Callback Check.
405
    if (!isset($this->config['auth_callbacks'][$auth])) {
406
      $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits.
407
    }
408
    // Authentication.
409
    if (!$this->config['auth_callbacks'][$auth]($this, $this->ci->security->xss_clean($_SERVER[$auth]))) {
410
      $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits.
411
    }
412
  }
413
  /**
414
   * [get_authorization_header description]
415
   * @return [type] [description]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
416
   */
417
  private function get_authorization_header():?string
418
  {
419
    if (isset($_SERVER['Authorization'])) {
420
      return trim($_SERVER["Authorization"]);
421
    } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI
422
      return trim($_SERVER["HTTP_AUTHORIZATION"]);
423
    } elseif (function_exists('apache_request_headers')) {
424
      $requestHeaders = apache_request_headers();
425
426
      // Avoid Surprises.
427
      $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
0 ignored issues
show
Bug introduced by
It seems like $requestHeaders can also be of type false; however, parameter $input of array_keys() 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

427
      $requestHeaders = array_combine(array_map('ucwords', array_keys(/** @scrutinizer ignore-type */ $requestHeaders)), array_values($requestHeaders));
Loading history...
Bug introduced by
It seems like $requestHeaders can also be of type false; however, parameter $input of array_values() 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

427
      $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values(/** @scrutinizer ignore-type */ $requestHeaders));
Loading history...
428
429
      if (isset($requestHeaders['Authorization'])) {
430
        return trim($requestHeaders['Authorization']);
431
      }
432
    }
433
    return null;
434
  }
435
436
  /**
437
   * [handle_response description]
438
   * @param int $code [description]
439
   */
440
  private function handle_response(int $code, $auth=null, ?string $errorReason=null):void
441
  {
442
    http_response_code($code);
443
    header("Content-Type: application/json");
444
    if (isset($this->config['response_callbacks'][$code])) {
445
      $this->config['response_callbacks'][$code]($auth, $errorReason);
446
    }
447
    if (ENVIRONMENT != 'testing') exit($code);
0 ignored issues
show
Bug introduced by
The constant ENVIRONMENT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
448
    throw new Exception("Error $code in $auth", $code);
449
  }
450
}
451
?>
0 ignored issues
show
Best Practice introduced by
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...
452