Skip to content

Commit

Permalink
Rate limiting
Browse files Browse the repository at this point in the history
* Upgrade styles config, remove unnesessary packages

* Add RateLimiter module

Use php 7.2 for a memcached driver

* UNdisable cache by default

* Close connection after sending a response

* Add comments

* Update README.md

* Remove always_populate_raw_post_data for php7

* add echo before die

* Small fixes

* Fix codestyle
  • Loading branch information
talyguryn authored Feb 26, 2020
1 parent 1e486e7 commit b1f1245
Show file tree
Hide file tree
Showing 15 changed files with 344 additions and 26 deletions.
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Made with :heart: by [CodeX](https://codex.so)
> :warning: Warning
>
> https://capella.pics is currently in beta. Uploading a large number of files is not supported.
>
> At this time any client cannot upload more than **3 images per minute**. This limit can be extened by access tokens. You can get it for your project later.
## Content

Expand Down Expand Up @@ -118,16 +120,17 @@ Each response will have at least `success` and `message` fields.

#### List of messages for failed requests

| Message | Description |
|----------------------------------|---------------------------------------|
| `Method not allowed` | Request method is not POST |
| `File or link is missing` | No expected data was found |
| `File is missing` | Filename is missing |
| `Link is missing` | Field link is empty |
| `Wrong source mime-type` | No support file with this mime-type |
| `Source is too big` | File size exceeds the limit |
| `Source is damaged` | Source has no data, size or mime-type |
| `Can't get headers for this URL` | Wrong url was passed |
| Code | Message | Description |
|-----------|----------------------------------|-------------------------------------------|
| **`400`** | `File or link is missing` | No expected data was found |
| **`400`** | `File is missing` | Filename is missing |
| **`400`** | `Link is missing` | Field link is empty |
| **`400`** | `Wrong source mime-type` | No support file with this mime-type |
| **`400`** | `Source is too big` | File size exceeds the limit |
| **`400`** | `Source is damaged` | Source has no data, size or mime-type |
| **`400`** | `Can't get headers for this URL` | Wrong url was passed |
| **`405`** | `Method not allowed` | Request method is not POST |
| **`429`** | `Too Many Requests` | Client has exceed plan limit. Retry later |

### Examples

Expand Down
9 changes: 8 additions & 1 deletion capella/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ HAWK_TOKEN=
# If you ARE NOT using docker: CACHE_HOST = localhost
CACHE_HOST=memcached
CACHE_PORT=11211
DISABLE_CACHE=True
#DISABLE_CACHE=True

### Rate limiter params
# QUOTA is a number of images could be uploaded per target time CYCLE in seconds
# For example: not more 3 images per 60 seconds (quota=3, cycle=60)
RATE_LIMITER_QUOTA=3
RATE_LIMITER_CYCLE=60
#DISABLE_RATE_LIMITER=True
14 changes: 14 additions & 0 deletions capella/src/Env.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ public static function getBool($param)
return $value;
}

/**
* Return int value for param from .env
*
* @param $param
*
* @return mixed
*/
public static function getInt($param)
{
$value = filter_var(self::get($param), FILTER_VALIDATE_INT);

return $value;
}

/**
* Return true if debug flag enabled in the .env config file
*
Expand Down
43 changes: 42 additions & 1 deletion capella/src/Methods.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public static function imageNameToId($name)
* If you store images in a cloud then upgrade this function
* for getting image's source from the cloud
*
* @param $id - image's id
* @param string $id - image's id
*
* @throws \Exception
*
Expand Down Expand Up @@ -107,4 +107,45 @@ public static function isAjax()
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');
}

/**
* Get request source IP address
*
* @return string
*/
public static function getRequestSourceIp()
{
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}

return $ip;
}

/**
* Get correct word for single or plural items
*
* @param integer $num - number of items
* @param string $nominative - word for 1 item
* @param string $genitive_singular - word for 4 items
* @param string $genitive_plural - word for 5 items
*
* @return string
*/
public static function getNumEnding($num, $nominative, $genitive_singular, $genitive_plural)
{
if ($num > 10 && (floor(($num % 100) / 10)) == 1) {
return $genitive_plural;
} else {
switch ($num % 10) {
case 1: return $nominative;
case 2: case 3: case 4: return $genitive_singular;
case 5: case 6: case 7: case 8: case 9: case 0: return $genitive_plural;
}
}
}
}
187 changes: 187 additions & 0 deletions capella/src/RateLimiter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php

use Cache\Cache;

/**
* @singleton
*
* Simple rate limiter module powered by Memcache
*
* @example
* if (RateLimiter::instance()->isEnabled()) {
* if (! RateLimiter::instance()->check($key)) {
* // ...reject request
* }
* }
*/
class RateLimiter
{
/**
* Is RateLimiter enabled and Cache works correctly
* @var bool
*/
private $isEnabled;

/**
* Number of images allowed to be uploaded for time interval (cycle)
*
* @var integer
*/
private $QUOTA;

/**
* Time interval defined as rate limiter cycle
*
* @var integer
*/
private $CYCLE;

/**
* @var RateLimiter
*/
private static $_instance = null;

public static function instance()
{
if (is_null(self::$_instance)) {
self::$_instance = new self();
}

return self::$_instance;
}

/**
* Private variable and public method to prevent variable outer changes
*
* @return bool
*/
public function isEnabled()
{
return $this->isEnabled;
}

/**
* Number of images allowed to be uploaded for time interval (cycle)
* Private variable and public method to prevent variable outer changes
*
* @return int
*/
public function QUOTA()
{
return $this->QUOTA;
}

/**
* Time interval defined as rate limiter cycle
* Private variable and public method to prevent variable outer changes
*
* @return int
*/
public function CYCLE()
{
return $this->CYCLE;
}

/**
* Check if client allowed to do an action
*
* @param string $key - client identifier
* @param int|null $quota - max number of images
* @param int|null $cycle - time interval
* @return bool|null - if request is allowed
*/
public function check($key, $quota = null, $cycle = null)
{
if (!$this->isEnabled) {
return null;
}

$quota = $quota ?: $this->QUOTA;
$cycle = $cycle ?: $this->CYCLE;

$defaultValue = 1;

$requestAllowed = true;

/** Try to get key */
$isCached = Cache::instance()->get($key);

if (is_null($isCached)) {
Cache::instance()->set($key, $defaultValue, $cycle);
return $requestAllowed;
}

if (intval($isCached) < $quota) {
Cache::instance()->increment($key);
return $requestAllowed;
}

return ! $requestAllowed;
}

/**
* Get error message with filled env params for quota and cycle
*
* @param int|null $quota - max number of images
* @param int|null $cycle - time interval
*
* @return string
*/
public function errorMessage($quota = null, $cycle = null)
{
$quota = $quota ?: $this->QUOTA;
$cycle = $cycle ?: $this->CYCLE;

$words = [
'image' => Methods::getNumEnding($quota, 'image', 'images', 'images'),
'second' => Methods::getNumEnding($cycle, 'second', 'seconds', 'seconds')
];

return "Sorry, you cannot upload more than ${quota} ${words['image']} per ${cycle} ${words['second']}.";
}

/**
* RateLimiter constructor
*/
private function __construct()
{
/** If rate limiter was disabled in .env */
if (Env::getBool('DISABLE_RATE_LIMITER')) {
return;
}

/**
* Define vars from env file
*/
$this->QUOTA = Env::getInt('RATE_LIMITER_QUOTA');
$this->CYCLE = Env::getInt('RATE_LIMITER_CYCLE');

if (!$this->QUOTA || !$this->CYCLE) {
throw new Exception('Rate limiter requires defined \'quota\' and \'cycle\' params. Check env file.');
}

/** Check if Cache module set up correctly */
$cache = \Cache\Cache::instance();
if (!$cache->isAlive()) {
throw new Exception('Rate limiter requires enabled cache. Check Memcache connection.');
}

/** RateLimiter is ready to work */
$this->isEnabled = true;
}

/**
* Prevent cloning of instance
*/
private function __clone()
{
}

private function __sleep()
{
}

private function __wakeup()
{
}
}
1 change: 0 additions & 1 deletion capella/src/Uploader.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<?php


/**
* Parent class, which describes acceptable extension,
* file size and methods that check these parameters.
Expand Down
2 changes: 2 additions & 0 deletions capella/src/api/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Response
public static function json($responseData)
{
echo json_encode($responseData);
die();
}

/**
Expand Down Expand Up @@ -76,5 +77,6 @@ public static function data($data, $cacheLifetime = 31536000)
}

echo $blob;
die();
}
}
Loading

0 comments on commit b1f1245

Please sign in to comment.