Skip to content

Commit 531e4c3

Browse files
committed
Adds rate limiting mechanism with configurable storage
Implements a rate limiting feature to track and enforce API usage limits based on response headers. Introduces a `RateLimiter` class with support for JSON and PDO-based storage through the `RateLimitStoreInterface`. Updates the `ApiClient` to integrate rate limiting, including handling scenarios where limits are exceeded. Adds tests for the rate limiting components to ensure reliability. Enhances compliance with API rate limits while providing flexibility for storage and behavior configuration.
1 parent d882928 commit 531e4c3

33 files changed

Lines changed: 924 additions & 85 deletions

.openapi-generator/config.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,36 @@ files:
2626
destinationFilename: Transactor.php
2727
bootstrap.php:
2828
folder: test
29+
RateLimiter.mustache:
30+
folder: lib/RateLimit
31+
destinationFilename: RateLimiter.php
32+
RateLimitStoreInterface.mustache:
33+
folder: lib/RateLimit
34+
destinationFilename: RateLimitStoreInterface.php
35+
SqlDialect.mustache:
36+
folder: lib/RateLimit
37+
destinationFilename: SqlDialect.php
38+
RateLimitMode.mustache:
39+
folder: lib/RateLimit
40+
destinationFilename: RateLimitMode.php
41+
RateLimitExceededException.mustache:
42+
folder: lib/RateLimit
43+
destinationFilename: RateLimitExceededException.php
44+
PdoRateLimitStore.mustache:
45+
folder: lib/RateLimit
46+
destinationFilename: PdoRateLimitStore.php
47+
RateLimitStore.mustache:
48+
folder: lib/RateLimit
49+
destinationFilename: RateLimitStore.php
50+
JsonRateLimitStore.mustache:
51+
folder: lib/RateLimit
52+
destinationFilename: JsonRateLimitStore.php
53+
RateLimiterTest.mustache:
54+
folder: test/RateLimit
55+
destinationFilename: RateLimiterTest.php
56+
RateLimitStoreInterfaceTest.mustache:
57+
folder: test/RateLimit
58+
destinationFilename: RateLimitStoreInterfaceTest.php
59+
SqlDialectTest.mustache:
60+
folder: test/RateLimit
61+
destinationFilename: SqlDialectTest.php
Lines changed: 127 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,102 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
/**
4-
*
6+
* This file is part of the MultiFlexi package
7+
*
8+
* https://github.com/VitexSoftware/php-vitexsoftware-rbczpremiumapi
9+
*
10+
* (c) Vítězslav Dvořák <http://vitexsoftware.com>
511
*
6-
* @author Vitex <vitex@hippy.cz>
7-
* @copyright 2023 Vitex@hippy.cz (G)
8-
*
9-
* PHP 8
12+
* For the full copyright and license information, please view the LICENSE
13+
* file that was distributed with this source code.
1014
*/
1115

1216
namespace VitexSoftware\Raiffeisenbank;
1317

18+
use VitexSoftware\Raiffeisenbank\RateLimit\RateLimiter;
19+
1420
/**
15-
* Description of ApiClient
21+
* Description of ApiClient.
1622
*
1723
* @author vitex
1824
*/
1925
class ApiClient extends \GuzzleHttp\Client
2026
{
21-
2227
/**
2328
* ClientID obtained from Developer Portal - when you registered your app with us.
24-
* @var string
2529
*/
2630
protected string $xIBMClientId = '';
2731
2832
/**
29-
* the end IP address of the client application (no server) in IPv4 or IPv6
30-
* format. If the bank client (your user) uses a browser by which he
31-
* accesses your server app, we need to know the IP address of his browser.
32-
* Always provide the closest IP address to the real end-user possible.
33-
* (optional)
34-
*
35-
* @var string
33+
* the end IP address of the client application (no server) in IPv4 or IPv6
34+
* format. If the bank client (your user) uses a browser by which he
35+
* accesses your server app, we need to know the IP address of his browser.
36+
* Always provide the closest IP address to the real end-user possible.
37+
* (optional).
3638
*/
3739
protected string $pSUIPAddress = '';
3840
3941
/**
4042
* Use mocking for api calls ?
41-
* @var boolean
4243
*/
4344
protected bool $mockMode = false;
45+
private RateLimiter $rateLimiter;
4446
4547
/**
46-
* @inheritDoc
47-
*
48+
* {@inheritDoc}
49+
*
4850
* $config['clientid'] - obtained from Developer Portal - when you registered your app with us.
4951
* $config['cert'] = ['/path/to/cert.p12','certificat password']
5052
* $config['clientpubip'] = the closest IP address to the real end-user
5153
* $config['mocking'] = true to use /rbcz/premium/mock/* endpoints
52-
*
53-
* @param array $config
54+
*
5455
* @throws \Exception CERT_FILE is not set
5556
* @throws \Exception CERT_PASS is not set
5657
*/
5758
public function __construct(array $config = [])
5859
{
59-
if (array_key_exists('clientid', $config) === false) {
60+
if (\array_key_exists('clientid', $config) === false) {
6061
$this->xIBMClientId = \Ease\Shared::cfg('XIBMCLIENTID');
6162
} else {
6263
$this->xIBMClientId = $config['clientid'];
6364
}
6465

65-
if (array_key_exists('cert', $config) === false) {
66+
if (\array_key_exists('cert', $config) === false) {
6667
$config['cert'] = [\Ease\Shared::cfg('CERT_FILE'), \Ease\Shared::cfg('CERT_PASS')];
68+
6769
if (empty($config['cert'][0])) {
6870
throw new \Exception('Certificate (CERT_FILE) not specified');
6971
}
72+
7073
if (empty($config['cert'][1])) {
7174
throw new \Exception('Certificate password (CERT_PASS) not specified');
7275
}
7376
}
7477

75-
if (array_key_exists('debug', $config) === false) {
78+
if (\array_key_exists('debug', $config) === false) {
7679
$config['debug'] = \Ease\Shared::cfg('API_DEBUG', false);
77-
}
78-
79-
if (array_key_exists('clientpubip', $config)){
80+
}
81+
82+
if (\array_key_exists('clientpubip', $config)) {
8083
$this->pSUIPAddress = $config['clientpubip'];
8184
}
8285

83-
if (array_key_exists('mocking', $config)){
84-
$this->mockMode = boolval($config['mocking']);
86+
if (\array_key_exists('mocking', $config)) {
87+
$this->mockMode = (bool) $config['mocking'];
8588
}
86-
89+
90+
$limitStore = new RateLimit\JsonRateLimitStore(sys_get_temp_dir().'/rbczpremiumapi_rates.json');
91+
92+
$this->rateLimiter = new RateLimiter($limitStore);
93+
8794
parent::__construct($config);
8895
}
8996

9097
/**
91-
* ClientID obtained from Developer Portal
92-
*
98+
* ClientID obtained from Developer Portal.
99+
*
93100
* @return string
94101
*/
95102
public function getXIBMClientId()
@@ -98,102 +105,152 @@ class ApiClient extends \GuzzleHttp\Client
98105
}
99106

100107
/**
101-
* Keep user public IP here
102-
*
108+
* Keep user public IP here.
109+
*
103110
* @return string
104111
*/
105112
public function getpSUIPAddress()
106113
{
107114
return $this->pSUIPAddress;
108-
}
115+
}
109116

110117
/**
111118
* Use mocking uri for api calls ?
112-
*
113-
* @return boolean
119+
*
120+
* @return bool
114121
*/
115122
public function getMockMode()
116123
{
117124
return $this->mockMode;
118125
}
119-
126+
120127
/**
121-
* Obtain Your current Public IP
122-
*
128+
* Obtain Your current Public IP.
129+
*
123130
* @deprecated since version 0.1 - Do not use in production Environment!
124-
*
131+
*
125132
* @return string
126133
*/
127134
public static function getPublicIP()
128135
{
129136
$curl = curl_init();
130-
curl_setopt($curl, CURLOPT_URL, "http://httpbin.org/ip");
131-
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
137+
curl_setopt($curl, \CURLOPT_URL, 'http://httpbin.org/ip');
138+
curl_setopt($curl, \CURLOPT_RETURNTRANSFER, 1);
132139
$output = curl_exec($curl);
133140
curl_close($curl);
134141
$ip = json_decode($output, true);
142+
135143
return $ip['origin'];
136144
}
137-
145+
138146
/**
139-
* Source Identifier
140-
*
147+
* Source Identifier.
148+
*
141149
* @deprecated since version 0.1 - Do not use in production Environment!
142-
*
150+
*
143151
* @return string
144152
*/
145153
public static function sourceString()
146154
{
147-
return substr(__FILE__ . '@' . gethostname(), -50);
155+
return substr(__FILE__.'@'.gethostname(), -50);
148156
}
149157

150158
/**
151-
* Try to check certificate readibilty
152-
*
159+
* Try to check certificate readibilty.
160+
*
161+
* @param string $certFile path to certificate
162+
* @param bool $die throw exception or return false ?
163+
*
153164
* @throws Exception - Certificate file not found
154-
*
155-
* @param string $certFile path to certificate
156-
* @param boolean $die throw exception or return false ?
157-
*
158-
* @return boolean certificate file
165+
*
166+
* @return bool certificate file
159167
*/
160168
public static function checkCertificatePresence(string $certFile, bool $die = false): bool
161169
{
162170
$found = false;
171+
163172
if ((file_exists($certFile) === false) || (is_readable($certFile) === false)) {
164-
$errMsg = 'Cannot read specified certificate file: ' . $certFile;
165-
fwrite(STDERR, $errMsg . PHP_EOL);
166-
if ($die){
173+
$errMsg = 'Cannot read specified certificate file: '.$certFile;
174+
fwrite(\STDERR, $errMsg.\PHP_EOL);
175+
176+
if ($die) {
167177
throw new \Exception($errMsg);
168178
}
169179
} else {
170180
$found = true;
171181
}
172-
return $found;
182+
183+
return $found;
173184
}
174-
175-
public static function checkCertificate($certFile,$password): bool {
176-
return self::checkCertificatePresence($certFile) && self::checkCertificatePassword($certFile,$password);
185+
186+
public static function checkCertificate($certFile, $password): bool
187+
{
188+
return self::checkCertificatePresence($certFile) && self::checkCertificatePassword($certFile, $password);
177189
}
178-
179-
public static function checkCertificatePassword(string $certFile, string $password): bool {
190+
191+
public static function checkCertificatePassword(string $certFile, string $password): bool
192+
{
180193
$certContent = file_get_contents($certFile);
194+
181195
if (openssl_pkcs12_read($certContent, $certs, $password) === false) {
182196
fwrite(\STDERR, 'Cannot read PKCS12 certificate file: '.$certFile.\PHP_EOL);
197+
183198
exit(1);
184199
}
200+
185201
return true;
186202
}
187-
188-
203+
189204
/**
190-
* Request Identifier
191-
*
205+
* Request Identifier.
206+
*
207+
* @todo Obtain using RateLimiter
208+
*
192209
* @deprecated since version 0.1 - Do not use in production Environment!
193-
*
210+
*
194211
* @return string
195212
*/
196-
public static function getxRequestId() {
197-
return substr(self::sourceString() . '#' . time(),-59);
213+
public static function getxRequestId()
214+
{
215+
return substr(self::sourceString().'#'.time(), -59);
216+
}
217+
218+
/**
219+
* Send an HTTP request.
220+
*
221+
* @param array $options Request options to apply to the given
222+
* request and to the transfer. See \GuzzleHttp\RequestOptions.
223+
*
224+
* @throws GuzzleException
225+
* @throws RateLimitExceededException
226+
*/
227+
public function send(\Psr\Http\Message\RequestInterface $request, array $options = []): \Psr\Http\Message\ResponseInterface
228+
{
229+
$this->rateLimiter->checkBeforeRequest($this->xIBMClientId);
230+
231+
$response = parent::send($request, $options);
232+
233+
$statusCode = $response->getStatusCode();
234+
$responseHeaders = $response->getHeaders();
235+
236+
if (isset($responseHeaders['x-ratelimit-remaining-second'])) {
237+
$remainingSecond = (int) $responseHeaders['x-ratelimit-remaining-second'][0];
238+
$remainingDay = (int) $responseHeaders['x-ratelimit-remaining-day'][0];
239+
240+
$timestamp = time();
241+
242+
$this->rateLimiter->handleRateLimits($this->xIBMClientId, $remainingSecond, $remainingDay, $timestamp);
243+
}
244+
245+
if ($statusCode === 429) { // 429 Too Many Requests
246+
if ($this->rateLimiter->isWaitMode()) {
247+
$this->rateLimiter->checkBeforeRequest($this->xIBMClientId);
248+
$response = parent::send($request, $options);
249+
} else {
250+
throw new RateLimitExceededException('Rate limit exceeded (HTTP 429)');
251+
}
252+
}
253+
254+
return $response;
198255
}
199256
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace VitexSoftware\Raiffeisenbank\RateLimit;
4+
5+
class JsonRateLimitStore implements RateLimitStoreInterface
6+
{
7+
// Add JSON storage logic for rate limits
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace VitexSoftware\Raiffeisenbank\RateLimit;
4+
5+
class PdoRateLimitStore implements RateLimitStoreInterface
6+
{
7+
// Add PDO storage logic for rate limits
8+
}

0 commit comments

Comments
 (0)