@@ -25,6 +25,9 @@ class ApiCache
2525 /** @var bool */
2626 private $ ruptureMode ;
2727
28+ /** @var int */
29+ private $ cacheExpirationForCleanIp ;
30+
2831 /** @var ApiClient */
2932 private $ apiClient ;
3033
@@ -39,10 +42,11 @@ public function __construct(ApiClient $apiClient = null)
3942 /**
4043 * Configure this instance.
4144 */
42- public function configure (AbstractAdapter $ adapter , bool $ ruptureMode , string $ apiUrl , int $ timeout , string $ userAgent , string $ token ): void
45+ public function configure (AbstractAdapter $ adapter , bool $ ruptureMode , string $ apiUrl , int $ timeout , string $ userAgent , string $ token, int $ cacheExpirationForCleanIp ): void
4346 {
4447 $ this ->adapter = $ adapter ;
4548 $ this ->ruptureMode = $ ruptureMode ;
49+ $ this ->cacheExpirationForCleanIp = $ cacheExpirationForCleanIp ;
4650
4751 $ this ->apiClient ->configure ($ apiUrl , $ timeout , $ userAgent , $ token );
4852 }
@@ -85,22 +89,89 @@ private function saveDeferred(CacheItem $item, int $ip, string $type, int $expir
8589 }
8690 }
8791
88- /*
92+ /**
93+ * Parse "duration" entries returned from API to a number of seconds.
94+ *
95+ * TODO P3 TEST
96+ * 9999h59m56.603445s
97+ * 10m33.3465483s
98+ * 33.3465483s
99+ * -285.876962ms
100+ * 33s'// should break!;
101+ */
102+ private static function parseDurationToSeconds (string $ duration ): int
103+ {
104+ $ re = '/(-?)(?:(?:(\d+)h)?(\d+)m)?(\d+).\d+(m?)s/m ' ;
105+ preg_match ($ re , $ duration , $ matches );
106+ if (!count ($ matches )) {
107+ throw new BouncerException ("Unable to parse the following duration: $ {$ duration }. " );
108+ };
109+ $ seconds = 0 ;
110+ if (null !== $ matches [2 ]) {
111+ $ seconds += ((int ) $ matches [1 ]) * 3600 ; // hours
112+ }
113+ if (null !== $ matches [3 ]) {
114+ $ seconds += ((int ) $ matches [2 ]) * 60 ; // minutes
115+ }
116+ if (null !== $ matches [4 ]) {
117+ $ seconds += ((int ) $ matches [1 ]); // seconds
118+ }
119+ if (null !== $ matches [5 ]) { // units in milliseconds
120+ $ seconds *= 0.001 ;
121+ }
122+ if (null !== $ matches [1 ]) { // negative
123+ $ seconds *= -1 ;
124+ }
125+ $ seconds = round ($ seconds );
126+
127+ return (int )$ seconds ;
128+ }
129+
130+
131+
132+ /**
133+ * Format a remediation item of a cache item.
134+ * This format use a minimal amount of data allowing less cache data consumption.
135+ *
136+ * TODO P3 TESTS
137+ */
138+ private function formatRemediationFromDecision (?array $ decision ): array
139+ {
140+ if (!$ decision ) {
141+ return ['clean ' , time () + $ this ->cacheExpirationForCleanIp , 0 ];
142+ }
143+
144+ return [
145+ $ decision ['type ' ], // ex: captcha
146+ time () + self ::parseDurationToSeconds ($ decision ['duration ' ]), // expiration timestamp
147+ $ decision ['id ' ],
148+
149+ /*
150+ TODO P3 useful to keep in cache?
151+ [
152+ $decision['origin'],// ex cscli
153+ $decision['scenario'],//ex: "manual 'captcha' from '25b9f1216f9344b780963bd281ae5573UIxCiwc74i2mFqK4'"
154+ $decision['scope'],// ex: IP
155+ ]
156+ */
157+ ];
158+ }
89159
90- Update the cached remediations from these new decisions.
160+ /**
161+ * Update the cached remediations from these new decisions.
91162
92- TODO P2 WRITE TESTS
93- 0 decisions
94- 3 known remediation type
95- 3 decisions but 1 unknown remediation type
96- 3 unknown remediation type
163+ * TODO P2 WRITE TESTS
164+ * 0 decisions
165+ * 3 known remediation type
166+ * 3 decisions but 1 unknown remediation type
167+ * 3 unknown remediation type
97168 */
98169 private function saveRemediations (array $ decisions ): bool
99170 {
100171 foreach ($ decisions as $ decision ) {
101172 $ ipRange = range ($ decision ['start_ip ' ], $ decision ['end_ip ' ]);
102173 foreach ($ ipRange as $ ip ) {
103- $ remediation = Remediation:: formatFromDecision ($ decision );
174+ $ remediation = $ this -> formatRemediationFromDecision ($ decision );
104175 $ item = $ this ->buildRemediationCacheItem ($ ip , $ remediation [0 ], $ remediation [1 ], $ remediation [2 ]);
105176 $ this ->saveDeferred ($ item , $ ip , $ remediation [0 ], $ remediation [1 ], $ remediation [2 ]);
106177 }
@@ -114,8 +185,14 @@ private function saveRemediations(array $decisions): bool
114185 */
115186 private function saveRemediationsForIp (array $ decisions , int $ ip ): void
116187 {
117- foreach ($ decisions as $ decision ) {
118- $ remediation = Remediation::formatFromDecision ($ decision );
188+ if (\count ($ decisions )) {
189+ foreach ($ decisions as $ decision ) {
190+ $ remediation = $ this ->formatRemediationFromDecision ($ decision );
191+ $ item = $ this ->buildRemediationCacheItem ($ ip , $ remediation [0 ], $ remediation [1 ], $ remediation [2 ]);
192+ $ this ->saveDeferred ($ item , $ ip , $ remediation [0 ], $ remediation [1 ], $ remediation [2 ]);
193+ }
194+ } else {
195+ $ remediation = $ this ->formatRemediationFromDecision (null );
119196 $ item = $ this ->buildRemediationCacheItem ($ ip , $ remediation [0 ], $ remediation [1 ], $ remediation [2 ]);
120197 $ this ->saveDeferred ($ item , $ ip , $ remediation [0 ], $ remediation [1 ], $ remediation [2 ]);
121198 }
@@ -164,17 +241,7 @@ public function pullUpdates(): void
164241 private function miss (int $ ip ): string
165242 {
166243 $ decisions = $ this ->apiClient ->getFilteredDecisions (['ip ' => long2ip ($ ip )]);
167-
168- if (!\count ($ decisions )) {
169- // 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-
173- return Remediation::formatFromDecision (null )[0 ];
174- }
175-
176244 $ this ->saveRemediationsForIp ($ decisions , $ ip );
177-
178245 return $ this ->hit ($ ip );
179246 }
180247
@@ -213,6 +280,6 @@ public function get(int $ip): ?string
213280 return $ this ->miss ($ ip );
214281 }
215282
216- return Remediation:: formatFromDecision (null )[0 ];
283+ return $ this -> formatRemediationFromDecision (null )[0 ];
217284 }
218285}
0 commit comments