Issues (71)

libraries/REST.php (1 issue)

a closing PHP tag ("?>") is never present.

Best Practice Minor
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;
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);
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
?>
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...
504