Passed
Push — master ( af0ddd...588379 )
by Francis
01:16
created

REST::basic_auth()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 5
rs 10
cc 4
nc 4
nop 0
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
   * [private description]
18
   * @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...
19
   */
20
  private $api_key_limit_column;
21
  /**
22
   * [private description]
23
   * @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...
24
   */
25
  private $api_key_column;
26
  /**
27
   * [private description]
28
   * @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...
29
   */
30
  private $per_hour;
31
  /**
32
   * [private description]
33
   * @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...
34
   */
35
  private $ip_per_hour;
36
  /**
37
   * [private description]
38
   * @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...
39
   */
40
  private $show_header;
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 $whitelist;
46
  /**
47
   * [private description]
48
   * @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...
49
   */
50
  private $checked_rate_limit = false;
51
  /**
52
   * [private description]
53
   * @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...
54
   */
55
  private $header_prefix;
56
  /**
57
   * [private description]
58
   * @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...
59
   */
60
  private $limit_api;
61
62
  /**
63
   * [public description]
64
   * @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...
65
   */
66
  public  $userId;
67
  /**
68
   * [public description]
69
   * @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...
70
   */
71
  public  $apiKeyHeader;
72
  /**
73
   * [public description]
74
   * @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...
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]
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...
91
   */
92
  function __construct($params=null) {
0 ignored issues
show
Unused Code introduced by
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

92
  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...
93
    $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

93
    $this->ci =& /** @scrutinizer ignore-call */ get_instance();
Loading history...
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;
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...
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.');
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

131
    /** @scrutinizer ignore-call */ 
132
    log_message('debug', 'REST Request Authenticated and REST Library Initialized.');
Loading history...
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.
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

146
      if (preg_match('#^'.$uri.'$#', /** @scrutinizer ignore-call */ uri_string())) $auths = $auth_array; // Assign Authentication Steps.
Loading history...
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) {
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...
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)) {
0 ignored issues
show
Bug introduced by
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

226
      if (in_array($this->ci->input->ip_address(), /** @scrutinizer ignore-type */ $this->whitelist)) {
Loading history...
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;
0 ignored issues
show
Bug introduced by
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

266
    if (in_array($this->ci->input->ip_address(), /** @scrutinizer ignore-type */ $this->whitelist)) return;
Loading history...
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]
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...
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));
0 ignored issues
show
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

324
      $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values(/** @scrutinizer ignore-type */ $requestHeaders));
Loading history...
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

324
      $requestHeaders = array_combine(array_map('ucwords', array_keys(/** @scrutinizer ignore-type */ $requestHeaders)), array_values($requestHeaders));
Loading history...
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
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...
Bug introduced by
The constant ENVIRONMENT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
343
    throw new Exception("Error $code in $auth", $code);
344
  }
345
}
346
?>
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...
347