Skip to content

Commit b451a66

Browse files
committed
lint pass
1 parent e4e61d0 commit b451a66

19 files changed

+279
-144
lines changed

README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The official PHP client for the CrowdSec APIs (LAPI or CAPI).
44

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

7-
## Getting started!
7+
## Getting started
88

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

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

1717
# Licence
1818

19-
MIT License. Details in the `./LICENSE` file.
19+
MIT License. Details in the `./LICENSE` file.
20+
21+
# TODO
22+
23+
Features:
24+
- [x] Fast API client
25+
- [x] LAPI Support
26+
- [x] Built-in support for the most known cache systems: Redis, Memcached, PhpFiles
27+
- [x] Rupture mode
28+
- [ ] Stream mode (alpha version)
29+
- [ ] Cap remediation level (ex: for sensitives websites: ban will be capped to captcha)
30+
- [ ] Direct CAPI support
31+
- [ ] Log events using monolog
32+
- [ ] PHP 5.6 retro compatibility (currenly PHP 7.2+)
33+
- [ ] Retrieve cache items with pagination
34+
- [ ] Release 1.0.0 version
35+
- [ ] Support more cache systems (Apcu, Couchbase, Doctrine, Pdo)
36+
37+
Code:
38+
- [x] Docker dev environment (Dockerized Crowdsec, Redis, Memcached, Composer, PHPUnit)
39+
- [x] Continuous Integration (CI, includes Integration Tests and Super Linter)
40+
- [x] Integration tests (with TDD)
41+
- [x] Documented (Static documentation, PHP Doc)
42+
- [ ] Continuous Delivery (CD)
43+
- [ ] Load tests (compare performances)
44+
- [ ] Report Code coverage
45+
- [ ] Setup Xdebug environment

docker/.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
LAPI_URL=http://crowdsec:8080
2-
REDIS_DSN=redis://redis:6379
3-
MEMCACHED_DSN=memcached://memcached:11211
2+
MEMCACHED_DSN=memcached://memcached:11211
3+
REDIS_DSN=redis://redis:6379

docker/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
FROM php:7.2-fpm-alpine
22

33
RUN apk update \
4-
&& apk add --no-cache git mysql-client curl libmcrypt libmcrypt-dev openssh-client icu-dev \
5-
libxml2-dev freetype-dev libpng-dev libjpeg-turbo-dev g++ make autoconf libmemcached-dev \
4+
&& 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 \
5+
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 \
66
&& docker-php-source extract \
77
&& pecl install xdebug redis memcached \
88
&& docker-php-ext-enable xdebug redis memcached \

docs/getting-started.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ Use the bouncer library (rupture mode)
1717
1818
1919
use CrowdSecBouncer\Bouncer;
20-
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
20+
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
2121
2222
$apiToken = getenv(DEFINE_YOUR_TOKEN);// Good practice: define this secret data in environment variables.
2323
24-
// Select the best cache adapter for your needs (Memcached, Redis, Apcu, Filesystem, Doctrine, PhpFileSystem, Couchbase, Pdo...)
24+
// Select the best cache adapter for your needs (Memcached, Redis, PhpFiles)
25+
// Note: You can try more cache system but we did not test them for now (Apcu, Filesystem, Doctrine, Couchbase, Pdo).
2526
// The full list is here: https://symfony.com/doc/current/components/cache.html#available-cache-adapters
26-
$cacheAdapter = new FilesystemAdapter();
27+
$cacheAdapter = new PhpFilesAdapter();
2728
2829
$bouncer = new Bouncer();
2930
$bouncer->configure(['api_token'=> $apiToken], $cacheAdapter);

docs/tests.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ We explain here how to run the tests.
88
1) Build crowdesc docker image
99
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1010

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

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

15-
git clone git@github.com:crowdsecurity/crowdsec.git && cd $_ && docker build -t crowdsec . && cd .. && rm -rf ./crowdsec
15+
.. code-block:: sh
16+
git clone --branch v1.0.0-rc4 git@github.com:crowdsecurity/crowdsec.git .tmp-crowdsec \
17+
&& docker build -t crowdsec:v1.0.0-rc4 ./.tmp-crowdsec \
18+
&& rm -rf ./.tmp-crowdsec
1619
1720
.. _2-install-composer-dependencies:
1821

src/ApiCache.php

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44

55
namespace CrowdSecBouncer;
66

7-
use Symfony\Component\Cache\Adapter\NullAdapter;
87
use Symfony\Component\Cache\Adapter\AbstractAdapter;
98
use Symfony\Component\Cache\CacheItem;
109

1110
/**
1211
* The cache mecanism to store every decisions from LAPI/CAPI. Symfony Cache component powered.
13-
*
12+
*
1413
* @author CrowdSec team
15-
* @link https://crowdsec.net CrowdSec Official Website
14+
*
15+
* @see https://crowdsec.net CrowdSec Official Website
16+
*
1617
* @copyright Copyright (c) 2020+ CrowdSec
1718
* @license MIT License
1819
*/
@@ -30,57 +31,57 @@ class ApiCache
3031
/** @var bool */
3132
private $warmedUp = false;
3233

33-
public function __construct(ApiClient $apiClient)
34+
public function __construct(ApiClient $apiClient = null)
3435
{
3536
$this->apiClient = $apiClient ?: new ApiClient();
3637
}
3738

3839
/**
3940
* Configure this instance.
4041
*/
41-
public function configure(AbstractAdapter $adapter, bool $ruptureMode, array $apiClientConfiguration)
42+
public function configure(AbstractAdapter $adapter, bool $ruptureMode, string $apiUrl, int $timeout, string $userAgent, string $token): void
4243
{
43-
$this->adapter = $adapter ?: new NullAdapter();
44+
$this->adapter = $adapter;
4445
$this->ruptureMode = $ruptureMode;
4546

46-
$this->apiClient->configure(
47-
$apiClientConfiguration['api_url'],
48-
$apiClientConfiguration['api_timeout'],
49-
$apiClientConfiguration['api_user_agent'],
50-
$apiClientConfiguration['api_token']
51-
);
47+
$this->apiClient->configure($apiUrl, $timeout, $userAgent, $token);
5248
}
5349

5450
/**
55-
* Build a Symfony Cache Item from a couple of IP and its computed remediation
51+
* Build a Symfony Cache Item from a couple of IP and its computed remediation.
5652
*/
57-
private function buildRemediationCacheItem(int $ip, array $remediation): CacheItem
53+
private function buildRemediationCacheItem(int $ip, string $type, int $expiration, int $decisionId): CacheItem
5854
{
59-
$item = $this->adapter->getItem((string)$ip);
60-
55+
$item = $this->adapter->getItem((string) $ip);
56+
6157
// Merge with existing remediations (if any).
6258
$remediations = $item->get();
6359
$remediations = $remediations ?: [];
64-
$remediations[$remediation[2]] = $remediation;// erase previous decision with the same id
65-
60+
$remediations[$decisionId] = [
61+
$type,
62+
$expiration,
63+
$decisionId,
64+
]; // erase previous decision with the same id
65+
6666
// Build the item lifetime in cache and sort remediations by priority
6767
$maxLifetime = max(array_column($remediations, 1));
6868
$prioritizedRemediations = Remediation::sortRemediationByPriority($remediations);
6969

7070
$item->set($prioritizedRemediations);
7171
$item->expiresAfter($maxLifetime);
72+
7273
return $item;
7374
}
7475

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

@@ -100,10 +101,11 @@ private function saveRemediations(array $decisions): bool
100101
$ipRange = range($decision['start_ip'], $decision['end_ip']);
101102
foreach ($ipRange as $ip) {
102103
$remediation = Remediation::formatFromDecision($decision);
103-
$item = $this->buildRemediationCacheItem($ip, $remediation);
104-
$this->saveDeferred($item, $ip, $remediation);
104+
$item = $this->buildRemediationCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
105+
$this->saveDeferred($item, $ip, $remediation[0], $remediation[1], $remediation[2]);
105106
}
106107
}
108+
107109
return $this->adapter->commit();
108110
}
109111

@@ -113,9 +115,9 @@ private function saveRemediations(array $decisions): bool
113115
private function saveRemediationsForIp(array $decisions, int $ip): void
114116
{
115117
foreach ($decisions as $decision) {
116-
$remediation = Remediation::formatFromDecision($decision);
117-
$item = $this->buildRemediationCacheItem($ip, $remediation);
118-
$this->saveDeferred($item, $ip, $remediation);
118+
$remediation = Remediation::formatFromDecision($decision);
119+
$item = $this->buildRemediationCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
120+
$this->saveDeferred($item, $ip, $remediation[0], $remediation[1], $remediation[2]);
119121
}
120122
$this->adapter->commit();
121123
}
@@ -124,7 +126,7 @@ private function saveRemediationsForIp(array $decisions, int $ip): void
124126
* Used in stream mode only.
125127
* Warm the cache up.
126128
* Used when the stream mode has just been activated.
127-
*
129+
*
128130
* TODO P2 test for overlapping decisions strategy (max expires, remediation ordered by priorities)
129131
*/
130132
public function warmUp(): void
@@ -138,7 +140,7 @@ public function warmUp(): void
138140
if ($newDecisions) {
139141
$this->warmedUp = $this->saveRemediations($newDecisions);
140142
if (!$this->warmedUp) {
141-
throw new BouncerException(`Unable to warm the cache up`);
143+
throw new BouncerException("Unable to warm the cache up");
142144
}
143145
}
144146
}
@@ -153,7 +155,7 @@ public function pullUpdates(): void
153155
// TODO P1 Finish stream mode with pull update + dont forget to delete old decisions!
154156
}
155157

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

166-
if (!count($decisions)) {
168+
if (!\count($decisions)) {
167169
// TODO P1 cache also the clean IP.
170+
//$item = $this->buildRemediationCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
171+
//$this->saveDeferred($item, $ip, $remediation[0], $remediation[1], $remediation[2]);
172+
168173
return Remediation::formatFromDecision(null)[0];
169174
}
170175

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

176-
177181
/**
178182
* Used in both mode (stream and ruptue).
179183
* This method formats the cached item as a remediation.
180184
* It returns the highest remediation level found.
181185
*/
182-
private function hit(int $ip): ?string
186+
private function hit(int $ip): string
183187
{
184-
$remediations = $this->adapter->getItem((string)$ip)->get();
188+
$remediations = $this->adapter->getItem((string) $ip)->get();
185189
// P2 TODO control before if date is not expired and if true, update cache item.
186-
return $remediations[0][0]; // 0: first remediation level, 0: the "type" string
190+
191+
// We apply array values first because keys are ids.
192+
$firstRemediation = array_values($remediations)[0];
193+
/** @var string */
194+
$firstRemediationString = $firstRemediation[0];
195+
196+
return $firstRemediationString;
187197
}
188198

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

200-
if ($this->adapter->hasItem((string)$ip)) {
210+
if ($this->adapter->hasItem((string) $ip)) {
201211
return $this->hit($ip);
202-
} else if ($this->ruptureMode) {
212+
} elseif ($this->ruptureMode) {
203213
return $this->miss($ip);
204214
}
215+
205216
return Remediation::formatFromDecision(null)[0];
206217
}
207218
}

src/ApiClient.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,25 @@
66

77
/**
88
* The LAPI/CAPI REST Client. This is used to retrieve decisions.
9-
*
9+
*
1010
* @author CrowdSec team
11-
* @link https://crowdsec.net CrowdSec Official Website
11+
*
12+
* @see https://crowdsec.net CrowdSec Official Website
13+
*
1214
* @copyright Copyright (c) 2020+ CrowdSec
1315
* @license MIT License
1416
*/
1517
class ApiClient
1618
{
19+
/**
20+
* @var RestClient
21+
*/
22+
private $restClient;
23+
1724
/**
1825
* Configure this instance.
1926
*/
20-
public function configure(string $baseUri, int $timeout, string $userAgent, string $token)
27+
public function configure(string $baseUri, int $timeout, string $userAgent, string $token): void
2128
{
2229
$this->restClient = new RestClient();
2330
$this->restClient->configure($baseUri, [
@@ -35,6 +42,7 @@ public function getFilteredDecisions(array $filter): array
3542
// TODO P2 keep results filtered for scope=ip or scope=range (we can't do anything with other scopes)
3643
$decisions = $this->restClient->request('/v1/decisions', $filter);
3744
$decisions = $decisions ?: [];
45+
3846
return $decisions;
3947
}
4048

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

0 commit comments

Comments
 (0)