Skip to content

Commit

Permalink
lint pass
Browse files Browse the repository at this point in the history
  • Loading branch information
mobula9 committed Dec 2, 2020
1 parent e4e61d0 commit b451a66
Show file tree
Hide file tree
Showing 19 changed files with 279 additions and 144 deletions.
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The official PHP client for the CrowdSec APIs (LAPI or CAPI).

This client helps to create CrowdSec bouncers for PHP applications or frameworks (e-commerce, blog, other apps...).

## Getting started!
## Getting started

View `docs/getting-started.md` to learn how to include this library in your project.

Expand All @@ -16,4 +16,30 @@ You will find the full documenation here: (...) TODO P2

# Licence

MIT License. Details in the `./LICENSE` file.
MIT License. Details in the `./LICENSE` file.

# TODO

Features:
- [x] Fast API client
- [x] LAPI Support
- [x] Built-in support for the most known cache systems: Redis, Memcached, PhpFiles
- [x] Rupture mode
- [ ] Stream mode (alpha version)
- [ ] Cap remediation level (ex: for sensitives websites: ban will be capped to captcha)
- [ ] Direct CAPI support
- [ ] Log events using monolog
- [ ] PHP 5.6 retro compatibility (currenly PHP 7.2+)
- [ ] Retrieve cache items with pagination
- [ ] Release 1.0.0 version
- [ ] Support more cache systems (Apcu, Couchbase, Doctrine, Pdo)

Code:
- [x] Docker dev environment (Dockerized Crowdsec, Redis, Memcached, Composer, PHPUnit)
- [x] Continuous Integration (CI, includes Integration Tests and Super Linter)
- [x] Integration tests (with TDD)
- [x] Documented (Static documentation, PHP Doc)
- [ ] Continuous Delivery (CD)
- [ ] Load tests (compare performances)
- [ ] Report Code coverage
- [ ] Setup Xdebug environment
4 changes: 2 additions & 2 deletions docker/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
LAPI_URL=http://crowdsec:8080
REDIS_DSN=redis://redis:6379
MEMCACHED_DSN=memcached://memcached:11211
MEMCACHED_DSN=memcached://memcached:11211
REDIS_DSN=redis://redis:6379
4 changes: 2 additions & 2 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
FROM php:7.2-fpm-alpine

RUN apk update \
&& apk add --no-cache git mysql-client curl libmcrypt libmcrypt-dev openssh-client icu-dev \
libxml2-dev freetype-dev libpng-dev libjpeg-turbo-dev g++ make autoconf libmemcached-dev \
&& apk add --no-cache git=2.26.2-r0 mysql-client=10.4.15-r0 curl=7.69.1-r1 libmcrypt=2.5.8-r8 libmcrypt-dev=2.5.8-r8 openssh-client=8.3_p1-r0 icu-dev=67.1-r0 \
libxml2-dev=2.9.10-r5 freetype-dev=2.10.4-r0 libpng-dev=1.6.37-r1 libjpeg-turbo-dev=2.0.5-r0 g++=9.3.0-r2 make=4.3-r0 autoconf=2.69-r2 libmemcached-dev=1.0.18-r4 \
&& docker-php-source extract \
&& pecl install xdebug redis memcached \
&& docker-php-ext-enable xdebug redis memcached \
Expand Down
7 changes: 4 additions & 3 deletions docs/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ Use the bouncer library (rupture mode)
use CrowdSecBouncer\Bouncer;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
$apiToken = getenv(DEFINE_YOUR_TOKEN);// Good practice: define this secret data in environment variables.
// Select the best cache adapter for your needs (Memcached, Redis, Apcu, Filesystem, Doctrine, PhpFileSystem, Couchbase, Pdo...)
// Select the best cache adapter for your needs (Memcached, Redis, PhpFiles)
// Note: You can try more cache system but we did not test them for now (Apcu, Filesystem, Doctrine, Couchbase, Pdo).
// The full list is here: https://symfony.com/doc/current/components/cache.html#available-cache-adapters
$cacheAdapter = new FilesystemAdapter();
$cacheAdapter = new PhpFilesAdapter();
$bouncer = new Bouncer();
$bouncer->configure(['api_token'=> $apiToken], $cacheAdapter);
Expand Down
9 changes: 6 additions & 3 deletions docs/tests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ We explain here how to run the tests.
1) Build crowdesc docker image
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

First we need to create the crowdsec docker image.
At this day, there is no crowdsec images on docker hub, so you have to build it by yourself.

.. code-block:: sh
TODO P3 when v1.0.0 will be release, get the latest stable version.

git clone [email protected]:crowdsecurity/crowdsec.git && cd $_ && docker build -t crowdsec . && cd .. && rm -rf ./crowdsec
.. code-block:: sh
git clone --branch v1.0.0-rc4 [email protected]:crowdsecurity/crowdsec.git .tmp-crowdsec \
&& docker build -t crowdsec:v1.0.0-rc4 ./.tmp-crowdsec \
&& rm -rf ./.tmp-crowdsec
.. _2-install-composer-dependencies:

Expand Down
87 changes: 49 additions & 38 deletions src/ApiCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@

namespace CrowdSecBouncer;

use Symfony\Component\Cache\Adapter\NullAdapter;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\CacheItem;

/**
* The cache mecanism to store every decisions from LAPI/CAPI. Symfony Cache component powered.
*
*
* @author CrowdSec team
* @link https://crowdsec.net CrowdSec Official Website
*
* @see https://crowdsec.net CrowdSec Official Website
*
* @copyright Copyright (c) 2020+ CrowdSec
* @license MIT License
*/
Expand All @@ -30,57 +31,57 @@ class ApiCache
/** @var bool */
private $warmedUp = false;

public function __construct(ApiClient $apiClient)
public function __construct(ApiClient $apiClient = null)
{
$this->apiClient = $apiClient ?: new ApiClient();
}

/**
* Configure this instance.
*/
public function configure(AbstractAdapter $adapter, bool $ruptureMode, array $apiClientConfiguration)
public function configure(AbstractAdapter $adapter, bool $ruptureMode, string $apiUrl, int $timeout, string $userAgent, string $token): void
{
$this->adapter = $adapter ?: new NullAdapter();
$this->adapter = $adapter;
$this->ruptureMode = $ruptureMode;

$this->apiClient->configure(
$apiClientConfiguration['api_url'],
$apiClientConfiguration['api_timeout'],
$apiClientConfiguration['api_user_agent'],
$apiClientConfiguration['api_token']
);
$this->apiClient->configure($apiUrl, $timeout, $userAgent, $token);
}

/**
* Build a Symfony Cache Item from a couple of IP and its computed remediation
* Build a Symfony Cache Item from a couple of IP and its computed remediation.
*/
private function buildRemediationCacheItem(int $ip, array $remediation): CacheItem
private function buildRemediationCacheItem(int $ip, string $type, int $expiration, int $decisionId): CacheItem
{
$item = $this->adapter->getItem((string)$ip);
$item = $this->adapter->getItem((string) $ip);

// Merge with existing remediations (if any).
$remediations = $item->get();
$remediations = $remediations ?: [];
$remediations[$remediation[2]] = $remediation;// erase previous decision with the same id

$remediations[$decisionId] = [
$type,
$expiration,
$decisionId,
]; // erase previous decision with the same id

// Build the item lifetime in cache and sort remediations by priority
$maxLifetime = max(array_column($remediations, 1));
$prioritizedRemediations = Remediation::sortRemediationByPriority($remediations);

$item->set($prioritizedRemediations);
$item->expiresAfter($maxLifetime);

return $item;
}

/**
* Save the cache without committing it to the cache system. Useful to improve performance when updating the cache.
*/
private function saveDeferred(CacheItem $item, int $ip, array $remediation): void
private function saveDeferred(CacheItem $item, int $ip, string $type, int $expiration, int $decisionId): void
{
$isQueued = $this->adapter->saveDeferred($item);
if (!$isQueued) {
$ipStr = long2Ip($ip);
throw new BouncerException(`Unable to save this deferred item in cache: ${$ipStr} =>$remediation[0] (for $remediation[1]sec)`);
$ipStr = long2ip($ip);
throw new BouncerException("Unable to save this deferred item in cache: ${$ipStr} =>$type (for $expiration sec, #$decisionId)");
}
}

Expand All @@ -100,10 +101,11 @@ private function saveRemediations(array $decisions): bool
$ipRange = range($decision['start_ip'], $decision['end_ip']);
foreach ($ipRange as $ip) {
$remediation = Remediation::formatFromDecision($decision);
$item = $this->buildRemediationCacheItem($ip, $remediation);
$this->saveDeferred($item, $ip, $remediation);
$item = $this->buildRemediationCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
$this->saveDeferred($item, $ip, $remediation[0], $remediation[1], $remediation[2]);
}
}

return $this->adapter->commit();
}

Expand All @@ -113,9 +115,9 @@ private function saveRemediations(array $decisions): bool
private function saveRemediationsForIp(array $decisions, int $ip): void
{
foreach ($decisions as $decision) {
$remediation = Remediation::formatFromDecision($decision);
$item = $this->buildRemediationCacheItem($ip, $remediation);
$this->saveDeferred($item, $ip, $remediation);
$remediation = Remediation::formatFromDecision($decision);
$item = $this->buildRemediationCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
$this->saveDeferred($item, $ip, $remediation[0], $remediation[1], $remediation[2]);
}
$this->adapter->commit();
}
Expand All @@ -124,7 +126,7 @@ private function saveRemediationsForIp(array $decisions, int $ip): void
* Used in stream mode only.
* Warm the cache up.
* Used when the stream mode has just been activated.
*
*
* TODO P2 test for overlapping decisions strategy (max expires, remediation ordered by priorities)
*/
public function warmUp(): void
Expand All @@ -138,7 +140,7 @@ public function warmUp(): void
if ($newDecisions) {
$this->warmedUp = $this->saveRemediations($newDecisions);
if (!$this->warmedUp) {
throw new BouncerException(`Unable to warm the cache up`);
throw new BouncerException("Unable to warm the cache up");
}
}
}
Expand All @@ -153,7 +155,7 @@ public function pullUpdates(): void
// TODO P1 Finish stream mode with pull update + dont forget to delete old decisions!
}

/**
/**
* Used in rupture mode only.
* This method is called when nothing has been found in cache for the requested IP.
* This call the API for decisions concerning the specified IP. Finally the result is stored.
Expand All @@ -163,8 +165,11 @@ private function miss(int $ip): string
{
$decisions = $this->apiClient->getFilteredDecisions(['ip' => long2ip($ip)]);

if (!count($decisions)) {
if (!\count($decisions)) {
// TODO P1 cache also the clean IP.
//$item = $this->buildRemediationCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
//$this->saveDeferred($item, $ip, $remediation[0], $remediation[1], $remediation[2]);

return Remediation::formatFromDecision(null)[0];
}

Expand All @@ -173,35 +178,41 @@ private function miss(int $ip): string
return $this->hit($ip);
}


/**
* Used in both mode (stream and ruptue).
* This method formats the cached item as a remediation.
* It returns the highest remediation level found.
*/
private function hit(int $ip): ?string
private function hit(int $ip): string
{
$remediations = $this->adapter->getItem((string)$ip)->get();
$remediations = $this->adapter->getItem((string) $ip)->get();
// P2 TODO control before if date is not expired and if true, update cache item.
return $remediations[0][0]; // 0: first remediation level, 0: the "type" string

// We apply array values first because keys are ids.
$firstRemediation = array_values($remediations)[0];
/** @var string */
$firstRemediationString = $firstRemediation[0];

return $firstRemediationString;
}

/**
* Request the cache for the specified IP.
*
* @return string The computed remediation string, or null if no decision was found.
*
* @return string the computed remediation string, or null if no decision was found
*/
public function get(int $ip): ?string
{
if (!$this->ruptureMode && !$this->warmedUp) {
throw new BouncerException('CrowdSec Bouncer configured in "stream" mode. Please warm the cache up before trying to access it.');
}

if ($this->adapter->hasItem((string)$ip)) {
if ($this->adapter->hasItem((string) $ip)) {
return $this->hit($ip);
} else if ($this->ruptureMode) {
} elseif ($this->ruptureMode) {
return $this->miss($ip);
}

return Remediation::formatFromDecision(null)[0];
}
}
16 changes: 13 additions & 3 deletions src/ApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@

/**
* The LAPI/CAPI REST Client. This is used to retrieve decisions.
*
*
* @author CrowdSec team
* @link https://crowdsec.net CrowdSec Official Website
*
* @see https://crowdsec.net CrowdSec Official Website
*
* @copyright Copyright (c) 2020+ CrowdSec
* @license MIT License
*/
class ApiClient
{
/**
* @var RestClient
*/
private $restClient;

/**
* Configure this instance.
*/
public function configure(string $baseUri, int $timeout, string $userAgent, string $token)
public function configure(string $baseUri, int $timeout, string $userAgent, string $token): void
{
$this->restClient = new RestClient();
$this->restClient->configure($baseUri, [
Expand All @@ -35,6 +42,7 @@ public function getFilteredDecisions(array $filter): array
// TODO P2 keep results filtered for scope=ip or scope=range (we can't do anything with other scopes)
$decisions = $this->restClient->request('/v1/decisions', $filter);
$decisions = $decisions ?: [];

return $decisions;
}

Expand All @@ -45,7 +53,9 @@ public function getFilteredDecisions(array $filter): array
public function getStreamedDecisions(bool $startup = false): array
{
// TODO P2 keep results filtered for scope=ip or scope=range (we can't do anything with other scopes)
/** @var array */
$decisionsDiff = $this->restClient->request('/v1/decisions/stream', ['startup' => $startup]);

return $decisionsDiff;
}
}
Loading

0 comments on commit b451a66

Please sign in to comment.