@@ -45,6 +45,16 @@ class ApiClient extends \GuzzleHttp\Client
4545 protected bool $ mockMode = false ;
4646 private RateLimiter $ rateLimiter ;
4747
48+ /**
49+ * Path to the certificate file used for mTLS authentication.
50+ */
51+ private string $ certFilePath = '' ;
52+
53+ /**
54+ * Cached certificate fingerprint for rate limiting.
55+ */
56+ private ?string $ certFingerprint = null ;
57+
4858 /**
4959 * Initialize the API client with configuration, certificate validation, and a rate limiter.
5060 *
@@ -87,6 +97,8 @@ public function __construct(array $config = [])
8797 }
8898 }
8999
100+ $ this ->certFilePath = $ config ['cert ' ][0 ];
101+
90102 if (\array_key_exists ('debug ' , $ config ) === false ) {
91103 $ config ['debug ' ] = \Ease \Shared::cfg ('API_DEBUG ' , false );
92104 }
@@ -231,9 +243,65 @@ public static function getxRequestId()
231243 return substr (self ::sourceString ().'# ' .time (), -59 );
232244 }
233245
246+ /**
247+ * Calculate and return the SHA1 fingerprint of the certificate used for mTLS.
248+ *
249+ * This fingerprint is used as the client identifier for rate limiting,
250+ * as rate limits in the API are tied to the certificate, not to X-IBM-Client-Id.
251+ *
252+ * @throws \Exception if unable to read or parse the certificate
253+ *
254+ * @return string SHA1 fingerprint of the certificate in lowercase hex format
255+ */
256+ public function getCertificateFingerprint (): string
257+ {
258+ if ($ this ->certFingerprint !== null ) {
259+ return $ this ->certFingerprint ;
260+ }
261+
262+ if (empty ($ this ->certFilePath )) {
263+ throw new \Exception ('Certificate file path not set ' );
264+ }
265+
266+ $ certContent = file_get_contents ($ this ->certFilePath );
267+
268+ if ($ certContent === false ) {
269+ throw new \Exception ('Unable to read certificate file: ' .$ this ->certFilePath );
270+ }
271+
272+ $ certs = [];
273+
274+ if (openssl_pkcs12_read ($ certContent , $ certs , $ this ->getConfig ()['cert ' ][1 ]) === false ) {
275+ throw new \Exception ('Unable to parse PKCS12 certificate: ' .openssl_error_string ());
276+ }
277+
278+ if (!isset ($ certs ['cert ' ])) {
279+ throw new \Exception ('Certificate not found in PKCS12 file ' );
280+ }
281+
282+ $ certResource = openssl_x509_read ($ certs ['cert ' ]);
283+
284+ if ($ certResource === false ) {
285+ throw new \Exception ('Unable to read X509 certificate: ' .openssl_error_string ());
286+ }
287+
288+ $ fingerprint = openssl_x509_fingerprint ($ certResource , 'sha1 ' );
289+
290+ if ($ fingerprint === false ) {
291+ throw new \Exception ('Unable to calculate certificate fingerprint: ' .openssl_error_string ());
292+ }
293+
294+ $ this ->certFingerprint = strtolower ($ fingerprint );
295+
296+ return $ this ->certFingerprint ;
297+ }
298+
234299 /**
235300 * Send an HTTP request while enforcing and updating client rate limits.
236301 *
302+ * Rate limits are enforced per certificate (not per X-IBM-Client-Id).
303+ * The certificate fingerprint is used as the client identifier.
304+ *
237305 * @param \Psr\Http\Message\RequestInterface $request the HTTP request to send
238306 * @param array $options Request options to apply to the transfer. See \GuzzleHttp\RequestOptions.
239307 *
@@ -244,7 +312,9 @@ public static function getxRequestId()
244312 */
245313 public function send (\Psr \Http \Message \RequestInterface $ request , array $ options = []): \Psr \Http \Message \ResponseInterface
246314 {
247- $ this ->rateLimiter ->checkBeforeRequest ($ this ->xIBMClientId );
315+ $ certFingerprint = $ this ->getCertificateFingerprint ();
316+
317+ $ this ->rateLimiter ->checkBeforeRequest ($ certFingerprint );
248318
249319 $ response = parent ::send ($ request , $ options );
250320
@@ -254,7 +324,7 @@ public function send(\Psr\Http\Message\RequestInterface $request, array $options
254324
255325 if ($ statusCode === 429 ) { // 429 Too Many Requests
256326 if ($ this ->rateLimiter ->isWaitMode ()) {
257- $ this ->rateLimiter ->checkBeforeRequest ($ this -> xIBMClientId );
327+ $ this ->rateLimiter ->checkBeforeRequest ($ certFingerprint );
258328 $ response = parent ::send ($ request , $ options );
259329
260330 $ this ->updateRateLimitsFromResponse ($ response );
@@ -268,13 +338,21 @@ public function send(\Psr\Http\Message\RequestInterface $request, array $options
268338 return $ response ;
269339 }
270340
341+ /**
342+ * Update rate limits from API response headers.
343+ *
344+ * Uses the certificate fingerprint as the client identifier for storing rate limits.
345+ *
346+ * @param \Psr\Http\Message\ResponseInterface $response the HTTP response containing rate limit headers
347+ */
271348 private function updateRateLimitsFromResponse (\Psr \Http \Message \ResponseInterface $ response ): void
272349 {
273350 if ($ response ->hasHeader ('x-ratelimit-remaining-second ' ) && $ response ->hasHeader ('x-ratelimit-remaining-day ' )) {
274351 $ remainingSecond = (int ) $ response ->getHeaderLine ('x-ratelimit-remaining-second ' );
275352 $ remainingDay = (int ) $ response ->getHeaderLine ('x-ratelimit-remaining-day ' );
276353 $ timestamp = time ();
277- $ this ->rateLimiter ->handleRateLimits ($ this ->xIBMClientId , $ remainingSecond , $ remainingDay , $ timestamp );
354+ $ certFingerprint = $ this ->getCertificateFingerprint ();
355+ $ this ->rateLimiter ->handleRateLimits ($ certFingerprint , $ remainingSecond , $ remainingDay , $ timestamp );
278356 }
279357 }
280358}
0 commit comments