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

libraries/REST.php (3 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
   * [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
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...
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
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