@@ -35,7 +35,7 @@ class ApiCache
35
35
private $ apiClient ;
36
36
37
37
/** @var bool */
38
- private $ warmedUp = false ;
38
+ private $ warmedUp ;
39
39
40
40
public function __construct (ApiClient $ apiClient = null , LoggerInterface $ logger )
41
41
{
@@ -46,14 +46,26 @@ public function __construct(ApiClient $apiClient = null, LoggerInterface $logger
46
46
/**
47
47
* Configure this instance.
48
48
*/
49
- public function configure (AbstractAdapter $ adapter , bool $ liveMode , string $ apiUrl , int $ timeout , string $ userAgent , string $ token , int $ cacheExpirationForCleanIp ): void
50
- {
49
+ public function configure (
50
+ AbstractAdapter $ adapter ,
51
+ bool $ liveMode ,
52
+ string $ apiUrl ,
53
+ int $ timeout ,
54
+ string $ userAgent ,
55
+ string $ token ,
56
+ int $ cacheExpirationForCleanIp
57
+ ): void {
51
58
$ this ->adapter = $ adapter ;
52
59
$ this ->liveMode = $ liveMode ;
53
60
$ this ->cacheExpirationForCleanIp = $ cacheExpirationForCleanIp ;
54
- $ this ->logger ->debug ('Api Cache adapter: ' . get_class ($ adapter ));
55
- $ this ->logger ->debug ('Api Cache mode: ' . ($ liveMode ? 'live ' : 'stream ' ));
61
+ $ this ->logger ->debug ('Api Cache adapter: ' . get_class ($ adapter ));
62
+ $ this ->logger ->debug ('Api Cache mode: ' . ($ liveMode ? 'live ' : 'stream ' ));
56
63
$ this ->logger ->debug ("Api Cache expiration for clean ips: $ cacheExpirationForCleanIp sec " );
64
+ $ cacheConfigItem = $ this ->adapter ->getItem ('cacheConfig ' );
65
+ $ cacheConfig = $ cacheConfigItem ->get ();
66
+ $ warmedUp = (is_array ($ cacheConfig ) && isset ($ cacheConfig ['warmed_up ' ]) && $ cacheConfig ['warmed_up ' ] === true );
67
+ $ this ->warmedUp = $ warmedUp ;
68
+ $ this ->logger ->debug ("Api Cache already warmed up: " . ($ this ->warmedUp ? 'true ' : 'false ' ));
57
69
58
70
$ this ->apiClient ->configure ($ apiUrl , $ timeout , $ userAgent , $ token );
59
71
}
@@ -66,13 +78,15 @@ private function addRemediationToCacheItem(string $ip, string $type, int $expira
66
78
$ item = $ this ->adapter ->getItem ($ ip );
67
79
68
80
// Merge with existing remediations (if any).
69
- $ remediations = $ item ->get ();
70
- $ remediations = $ remediations ?: [];
81
+ $ remediations = $ item ->isHit () ? $ item ->get () : [];
71
82
72
83
$ index = array_search (Constants::REMEDIATION_BYPASS , array_column ($ remediations , 0 ));
73
84
if (false !== $ index ) {
74
- $ this ->logger ->debug ("cache# $ ip: Previously clean IP but now bad, remove the " .Constants::REMEDIATION_BYPASS ." remediation immediately " );
75
- unset($ remediations [$ index ]);
85
+ $ this ->logger ->debug (
86
+ "cache# $ ip: Previously clean IP but now bad, remove the " .
87
+ Constants::REMEDIATION_BYPASS . " remediation immediately "
88
+ );
89
+ unset($ remediations [$ index ]);
76
90
}
77
91
78
92
$ remediations [] = [
@@ -84,45 +98,44 @@ private function addRemediationToCacheItem(string $ip, string $type, int $expira
84
98
// Build the item lifetime in cache and sort remediations by priority
85
99
$ maxLifetime = max (array_column ($ remediations , 1 ));
86
100
$ prioritizedRemediations = Remediation::sortRemediationByPriority ($ remediations );
87
-
88
- //$this->logger->debug("Decision $decisionId added to cache item $ip with lifetime $maxLifetime. Now it looks like:");
89
- //dump($prioritizedRemediations);
101
+
90
102
$ item ->set ($ prioritizedRemediations );
91
103
$ item ->expiresAfter ($ maxLifetime );
92
104
93
105
// Save the cache without committing it to the cache system.
94
106
// Useful to improve performance when updating the cache.
95
107
if (!$ this ->adapter ->saveDeferred ($ item )) {
96
- throw new BouncerException ("cache# $ ip: Unable to save this deferred item in cache: $ type for $ expiration sec, (decision $ decisionId) " );
108
+ throw new BouncerException (
109
+ "cache# $ ip: Unable to save this deferred item in cache: " .
110
+ "$ type for $ expiration sec, (decision $ decisionId) "
111
+ );
97
112
}
98
113
}
99
114
100
115
/**
101
116
* Remove a decision from a Symfony Cache Item identified by ip
102
117
*/
103
- private function removeDecisionFromRemediationItem (string $ ip , int $ decisionId ): void
118
+ private function removeDecisionFromRemediationItem (string $ ip , int $ decisionId ): bool
104
119
{
105
120
//$this->logger->debug("Remove decision $decisionId from the cache item matching ip ".$ip);
106
121
$ item = $ this ->adapter ->getItem ($ ip );
107
122
$ remediations = $ item ->get ();
108
- //dump($remediations);
109
123
110
124
$ index = false ;
111
125
if ($ remediations ) {
112
126
$ index = array_search ($ decisionId , array_column ($ remediations , 2 ));
113
127
}
114
-
128
+
129
+ // If decision was not found for this cache item early return.
115
130
if (false === $ index ) {
116
- // TODO P3 this seems to be a bug from LAPI;-. Investigate.
117
- $ this ->logger ->info ("cache# $ ip: decision $ decisionId not found in cache. " );
118
- return ;
131
+ return false ;
119
132
}
120
133
unset($ remediations [$ index ]);
121
134
122
135
if (!$ remediations ) {
123
136
$ this ->logger ->debug ("cache# $ ip: No more remediation for cache. Let's remove the cache item " );
124
137
$ this ->adapter ->delete ($ ip );
125
- return ;
138
+ return true ;
126
139
}
127
140
// Build the item lifetime in cache and sort remediations by priority
128
141
$ maxLifetime = max (array_column ($ remediations , 1 ));
@@ -136,6 +149,7 @@ private function removeDecisionFromRemediationItem(string $ip, int $decisionId):
136
149
throw new BouncerException ("cache# $ ip: Unable to save item " );
137
150
}
138
151
$ this ->logger ->debug ("cache# $ ip: Decision $ decisionId successfuly removed -deferred- " );
152
+ return true ;
139
153
}
140
154
141
155
/**
@@ -156,19 +170,19 @@ private static function parseDurationToSeconds(string $duration): int
156
170
throw new BouncerException ("Unable to parse the following duration: $ {$ duration }. " );
157
171
};
158
172
$ seconds = 0 ;
159
- if (null !== $ matches [2 ]) {
173
+ if (isset ( $ matches [2 ]) ) {
160
174
$ seconds += ((int ) $ matches [1 ]) * 3600 ; // hours
161
175
}
162
- if (null !== $ matches [3 ]) {
176
+ if (isset ( $ matches [3 ]) ) {
163
177
$ seconds += ((int ) $ matches [2 ]) * 60 ; // minutes
164
178
}
165
- if (null !== $ matches [4 ]) {
179
+ if (isset ( $ matches [4 ]) ) {
166
180
$ seconds += ((int ) $ matches [1 ]); // seconds
167
181
}
168
- if (null !== $ matches [5 ]) { // units in milliseconds
182
+ if (isset ( $ matches [5 ]) ) { // units in milliseconds
169
183
$ seconds *= 0.001 ;
170
184
}
171
- if (null !== $ matches [1 ]) { // negative
185
+ if (isset ( $ matches [1 ]) ) { // negative
172
186
$ seconds *= -1 ;
173
187
}
174
188
$ seconds = round ($ seconds );
@@ -197,6 +211,15 @@ private function formatRemediationFromDecision(?array $decision): array
197
211
];
198
212
}
199
213
214
+ private function defferUpdateCacheConfig (array $ config ): void
215
+ {
216
+ $ cacheConfigItem = $ this ->adapter ->getItem ('cacheConfig ' );
217
+ $ cacheConfig = $ cacheConfigItem ->isHit () ? $ cacheConfigItem ->get () : [];
218
+ $ cacheConfig = array_replace_recursive ($ cacheConfig , $ config );
219
+ $ cacheConfigItem ->set ($ cacheConfig );
220
+ $ this ->adapter ->saveDeferred ($ cacheConfigItem );
221
+ }
222
+
200
223
/**
201
224
* Update the cached remediations from these new decisions.
202
225
@@ -209,24 +232,40 @@ private function formatRemediationFromDecision(?array $decision): array
209
232
private function saveRemediations (array $ decisions ): bool
210
233
{
211
234
foreach ($ decisions as $ decision ) {
212
- $ ipRange = array_map ('long2ip ' , range ($ decision ['start_ip ' ], $ decision ['end_ip ' ]));
213
- $ remediation = $ this ->formatRemediationFromDecision ($ decision );
214
- foreach ($ ipRange as $ ip ) {
215
- $ this ->addRemediationToCacheItem ($ ip , $ remediation [0 ], $ remediation [1 ], $ remediation [2 ]);
235
+ if (is_int ($ decision ['start_ip ' ]) && is_int ($ decision ['end_ip ' ])) {
236
+ $ ipRange = array_map ('long2ip ' , range ($ decision ['start_ip ' ], $ decision ['end_ip ' ]));
237
+ $ remediation = $ this ->formatRemediationFromDecision ($ decision );
238
+ foreach ($ ipRange as $ ip ) {
239
+ $ this ->addRemediationToCacheItem ($ ip , $ remediation [0 ], $ remediation [1 ], $ remediation [2 ]);
240
+ }
216
241
}
217
242
}
218
243
219
- return $ this ->adapter ->commit ();
244
+ $ warmedUp = $ this ->adapter ->commit ();
245
+
246
+ // Store the fact that the cache has been warmed up.
247
+ $ this ->defferUpdateCacheConfig (['warmed_up ' => $ warmedUp ]);
248
+
249
+ return $ warmedUp ;
220
250
}
221
251
222
252
private function removeRemediations (array $ decisions ): bool
223
253
{
224
254
foreach ($ decisions as $ decision ) {
225
- $ ipRange = array_map ('long2ip ' , range ($ decision ['start_ip ' ], $ decision ['end_ip ' ]));
226
- $ this ->logger ->debug ('decision# ' .$ decision ['id ' ].': remove for IPs ' .join (', ' , $ ipRange ));
227
- $ remediation = $ this ->formatRemediationFromDecision ($ decision );
228
- foreach ($ ipRange as $ ip ) {
229
- $ this ->removeDecisionFromRemediationItem ($ ip , $ remediation [2 ]);
255
+ if (is_int ($ decision ['start_ip ' ]) && is_int ($ decision ['end_ip ' ])) {
256
+ $ ipRange = array_map ('long2ip ' , range ($ decision ['start_ip ' ], $ decision ['end_ip ' ]));
257
+ $ this ->logger ->debug ('decision# ' . $ decision ['id ' ] . ': remove for IPs ' . join (', ' , $ ipRange ));
258
+ $ success = true ;
259
+ foreach ($ ipRange as $ ip ) {
260
+ if (!$ this ->removeDecisionFromRemediationItem ($ ip , $ decision ['id ' ])) {
261
+ $ success = false ;
262
+ }
263
+ }
264
+ if (!$ success ) {
265
+ // The API may return stale deletion events due to API design.
266
+ // Ignoring them is therefore not a problem.
267
+ $ this ->logger ->debug ("Decision " . $ decision ['id ' ] . " not found in cache for one or more items. " );
268
+ }
230
269
}
231
270
}
232
271
return $ this ->adapter ->commit ();
@@ -261,7 +300,6 @@ public function warmUp(): void
261
300
$ this ->logger ->info ('Warming the cache up ' );
262
301
$ startup = true ;
263
302
$ decisionsDiff = $ this ->apiClient ->getStreamedDecisions ($ startup );
264
- //dump($decisionsDiff);
265
303
$ newDecisions = $ decisionsDiff ['new ' ];
266
304
267
305
$ this ->adapter ->clear ();
@@ -288,7 +326,6 @@ public function pullUpdates(): void
288
326
}
289
327
290
328
$ decisionsDiff = $ this ->apiClient ->getStreamedDecisions ();
291
- //dump($decisionsDiff);
292
329
$ newDecisions = $ decisionsDiff ['new ' ];
293
330
$ deletedDecisions = $ decisionsDiff ['deleted ' ];
294
331
@@ -346,9 +383,11 @@ private function hit(string $ip): string
346
383
*/
347
384
public function get (string $ ip ): string
348
385
{
349
- $ this ->logger ->debug ('IP to check: ' . $ ip );
386
+ $ this ->logger ->debug ('IP to check: ' . $ ip );
350
387
if (!$ this ->liveMode && !$ this ->warmedUp ) {
351
- throw new BouncerException ('CrowdSec Bouncer configured in "stream" mode. Please warm the cache up before trying to access it. ' );
388
+ throw new BouncerException (
389
+ 'CrowdSec Bouncer configured in "stream" mode. Please warm the cache up before trying to access it. '
390
+ );
352
391
}
353
392
354
393
if ($ this ->adapter ->hasItem ($ ip )) {
@@ -358,7 +397,5 @@ public function get(string $ip): string
358
397
$ this ->logger ->debug ("Cache miss for IP: $ ip " );
359
398
return $ this ->miss ($ ip );
360
399
}
361
-
362
- return $ this ->formatRemediationFromDecision (null )[0 ];
363
400
}
364
401
}
0 commit comments