Issues (1)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/GuzzleRetryMiddleware.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * Guzzle Retry Middleware Library
5
 *
6
 * @license http://opensource.org/licenses/MIT
7
 * @link https://github.com/caseyamcl/guzzle_retry_middleware
8
 * @version 2.0
9
 * @package caseyamcl/guzzle_retry_middleware
10
 * @author Casey McLaughlin <[email protected]>
11
 *
12
 * For the full copyright and license information, please view the LICENSE
13
 * file that was distributed with this source code.
14
 *
15
 * ------------------------------------------------------------------
16
 */
17
18
declare(strict_types=1);
19
20
namespace GuzzleRetry;
21
22
use Closure;
23
use DateTime;
24
use GuzzleHttp\Exception\BadResponseException;
25
use GuzzleHttp\Exception\ConnectException;
26
use GuzzleHttp\Promise\Promise;
27
use Psr\Http\Message\RequestInterface;
28
use Psr\Http\Message\ResponseInterface;
29
30
use function call_user_func;
31
use function call_user_func_array;
32
use function GuzzleHttp\Promise\rejection_for;
33
use function in_array;
34
use function is_callable;
35
36
/**
37
 * Retry After Middleware
38
 *
39
 * Guzzle 6 middleware that retries requests when encountering responses
40
 * with certain conditions (429 or 503).  This middleware also respects
41
 * the `RetryAfter` header
42
 *
43
 * @author Casey McLaughlin <[email protected]>
44
 */
45
class GuzzleRetryMiddleware
46
{
47
    // HTTP date format
48
    public const DATE_FORMAT = 'D, d M Y H:i:s T';
49
50
    // Default retry header (off by default; configurable)
51
    public const RETRY_HEADER = 'X-Retry-Counter';
52
53
    // Default retry-after header
54
    public const RETRY_AFTER_HEADER = 'Retry-After';
55
56
    /**
57
     * @var array<mixed>
58
     */
59
    private $defaultOptions = [
60
61
        // Retry enabled.  Toggle retry on or off per request
62
        'retry_enabled'                    => true,
63
64
        // If server doesn't provide a Retry-After header, then set a default back-off delay
65
        // NOTE: This can either be a float, or it can be a callable that returns a (accepts count and response|null)
66
        'default_retry_multiplier'         => 1.5,
67
68
        // Set a maximum number of attempts per request
69
        'max_retry_attempts'               => 10,
70
71
        // Maximum allowable timeout seconds
72
        'max_allowable_timeout_secs'       => null,
73
74
        // Set this to TRUE to retry only if the HTTP Retry-After header is specified
75
        'retry_only_if_retry_after_header' => false,
76
77
        // Only retry when status is equal to these response codes
78
        'retry_on_status'                  => ['429', '503'],
79
80
        // Callback to trigger before delay occurs (accepts count, delay, request, response, options)
81
        'on_retry_callback'                => null,
82
83
        // Retry on connect timeout?
84
        'retry_on_timeout'                 => false,
85
86
        // Add the number of retries to an X-Header
87
        'expose_retry_header'              => false,
88
89
        // The header key
90
        'retry_header'                     => self::RETRY_HEADER,
91
92
        // The retry after header key
93
        'retry_after_header'               => self::RETRY_AFTER_HEADER,
94
95
        // Date format
96
        'retry_after_date_format'          => self::DATE_FORMAT
97
    ];
98
99
    /**
100
     * @var callable
101
     */
102
    private $nextHandler;
103
104
    /**
105
     * Provides a closure that can be pushed onto the handler stack
106
     *
107 75
     * Example:
108
     * <code>$handlerStack->push(GuzzleRetryMiddleware::factory());</code>
109
     *
110 75
     * @param array<mixed> $defaultOptions
111 75
     * @return Closure
112
     */
113
    public static function factory(array $defaultOptions = []): Closure
114
    {
115
        return function (callable $handler) use ($defaultOptions) {
116
            return new static($handler, $defaultOptions);
117
        };
118
    }
119
120 78
    /**
121
     * GuzzleRetryMiddleware constructor.
122 78
     *
123 78
     * @param callable $nextHandler
124 78
     * @param array<mixed> $defaultOptions
125
     */
126
    final public function __construct(callable $nextHandler, array $defaultOptions = [])
127
    {
128
        $this->nextHandler = $nextHandler;
129
        $this->defaultOptions = array_replace($this->defaultOptions, $defaultOptions);
130
    }
131 75
132
    /**
133
     * @param RequestInterface $request
134 75
     * @param array<mixed> $options
135
     * @return Promise
136
     */
137 75
    public function __invoke(RequestInterface $request, array $options): Promise
138 75
    {
139
        // Combine options with defaults specified by this middleware
140
        $options = array_replace($this->defaultOptions, $options);
141
142 75
        // Set the retry count if not already set
143 75
        if (! isset($options['retry_count'])) {
144 75
            $options['retry_count'] = 0;
145 75
        }
146 75
147
        $next = $this->nextHandler;
148
        return $next($request, $options)
149
            ->then(
150
                $this->onFulfilled($request, $options),
151
                $this->onRejected($request, $options)
152
            );
153
    }
154
155
    /**
156
     * No exceptions were thrown during processing
157
     *
158
     * Depending on where this middleware is in the stack, the response could still
159
     * be unsuccessful (e.g. 429 or 503), so check to see if it should be retried
160 75
     *
161
     * @param RequestInterface $request
162
     * @param array<mixed> $options
163 63
     * @return callable
164 48
     */
165 63
    protected function onFulfilled(RequestInterface $request, array $options): callable
166 75
    {
167
        return function (ResponseInterface $response) use ($request, $options) {
168
            return $this->shouldRetryHttpResponse($options, $response)
169
                ? $this->doRetry($request, $options, $response)
170
                : $this->returnResponse($options, $response);
171
        };
172
    }
173
174
    /**
175
     * An exception or error was thrown during processing
176
     *
177
     * If the reason is a BadResponseException exception, check to see if
178
     * the request can be retried.  Otherwise, pass it on.
179 75
     *
180
     * @param RequestInterface $request
181
     * @param array<mixed> $options
182
     * @return callable
183 18
     */
184 3
    protected function onRejected(RequestInterface $request, array $options): callable
185 3
    {
186
        return function ($reason) use ($request, $options) {
187
            // If was bad response exception, test if we retry based on the response headers
188 15
            if ($reason instanceof BadResponseException) {
189
                if ($this->shouldRetryHttpResponse($options, $reason->getResponse())) {
190 12
                    return $this->doRetry($request, $options, $reason->getResponse());
191 6
                }
192
            // If this was a connection exception, test to see if we should retry based on connect timeout rules
193
            } elseif ($reason instanceof ConnectException) {
194
                // If was another type of exception, test if we should retry based on timeout rules
195
                if ($this->shouldRetryConnectException($options)) {
196 12
                    return $this->doRetry($request, $options);
197 75
                }
198
            }
199
200
            // If made it here, then we have decided not to retry the request
201
            // Future-proofing this; remove when bumping minimum Guzzle version to 7.0
202
            if (class_exists('\GuzzleHttp\Promise\Create')) {
203
                return \GuzzleHttp\Promise\Create::rejectionFor($reason);
204
            } else {
205
                return rejection_for($reason);
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\Promise\rejection_for() has been deprecated with message: rejection_for will be removed in guzzlehttp/promises:2.0. Use Create::rejectionFor instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
206 12
            }
207
        };
208 12
    }
209 12
210 12
    /**
211
     * Decide whether or not to retry on connect exception
212
     *
213
     * @param array<mixed> $options
214
     * @return bool
215
     */
216
    protected function shouldRetryConnectException(array $options): bool
217
    {
218
        return $options['retry_enabled']
219
            && ($options['retry_on_timeout'] ?? false)
220
            && $this->countRemainingRetries($options) > 0;
221
    }
222
223
    /**
224
     * Check to see if a request can be retried
225 63
     *
226
     * This checks two things:
227 63
     *
228
     * 1. The response status code against the status codes that should be retried
229 21
     * 2. The number of attempts made thus far for this request
230 63
     *
231 63
     * @param array<mixed> $options
232 63
     * @param ResponseInterface|null $response
233 18
     * @return bool  TRUE if the response should be retried, FALSE if not
234
     */
235
    protected function shouldRetryHttpResponse(array $options, ?ResponseInterface $response = null): bool
236
    {
237 60
        $statuses = array_map('\intval', (array) $options['retry_on_status']);
238
        $hasRetryAfterHeader = $response ? $response->hasHeader('Retry-After') : false;
239
240
        switch (true) {
241
            case $options['retry_enabled'] === false:
242
            case $this->countRemainingRetries($options) === 0: // No Retry-After header, and it is required?  Give up
243
            case (! $hasRetryAfterHeader && $options['retry_only_if_retry_after_header']):
244
                return false;
245
246 66
            // Conditions met; see if status code matches one that can be retried
247
            default:
248 66
                $statusCode = $response ? $response->getStatusCode() : 0;
249
                return in_array($statusCode, $statuses, true);
250 66
        }
251 66
    }
252 66
253
    /**
254 66
     * Count the number of retries remaining.  Always returns 0 or greater.
255
     *
256
     * @param array<mixed> $options
257
     * @return int
258
     */
259
    protected function countRemainingRetries(array $options): int
260
    {
261
        $retryCount  = isset($options['retry_count']) ? (int) $options['retry_count'] : 0;
262
263
        $numAllowed  = isset($options['max_retry_attempts'])
264
            ? (int) $options['max_retry_attempts']
265
            : $this->defaultOptions['max_retry_attempts'];
266
267 57
        return (int) max([$numAllowed - $retryCount, 0]);
268
    }
269
270 57
    /**
271
     * Retry the request
272
     *
273 57
     * Increments the retry count, determines the delay (timeout), executes callbacks, sleeps, and re-send the request
274
     *
275
     * @param RequestInterface $request
276 57
     * @param array<mixed> $options
277
     * @param ResponseInterface|null $response
278 48
     * @return Promise
279
     */
280 48
    protected function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null): Promise
281 48
    {
282 48
        // Increment the retry count
283 48
        ++$options['retry_count'];
284 48
285
        // Determine the delay timeout
286
        $delayTimeout = $this->determineDelayTimeout($options, $response);
287
288
        // Callback?
289
        if ($options['on_retry_callback']) {
290 57
            call_user_func_array(
291
                $options['on_retry_callback'],
292
                [
293 57
                    (int) $options['retry_count'],
294
                    (float) $delayTimeout,
295
                    &$request,
296
                    &$options,
297
                    $response
298
                ]
299
            );
300
        }
301 63
302
        // Delay!
303
        usleep((int) ($delayTimeout * 1e6));
304 63
305 63
        // Return
306
        return $this($request, $options);
307 60
    }
308
309
    /**
310 3
     * @param array<mixed> $options
311
     * @param ResponseInterface $response
312
     * @return ResponseInterface
313
     */
314
    protected function returnResponse(array $options, ResponseInterface $response): ResponseInterface
315
    {
316
        if ($options['expose_retry_header'] === false || $options['retry_count'] === 0) {
317
            return $response;
318
        }
319
320
        return $response->withHeader($options['retry_header'], $options['retry_count']);
321
    }
322
323 57
    /**
324
     * Determine the delay timeout
325 57
     *
326
     * Attempts to read and interpret the configured retry after header, or defaults
327 3
     * to a built-in incremental back-off algorithm.
328 3
     *
329 3
     * @param array<mixed> $options
330
     * @param ResponseInterface|null $response
331
     * @return float  Delay timeout, in seconds
332 54
     */
333
    protected function determineDelayTimeout(array $options, ?ResponseInterface $response = null): float
334
    {
335
        if (is_callable($options['default_retry_multiplier'])) {
336
            $defaultDelayTimeout = (float) call_user_func(
337 57
                $options['default_retry_multiplier'],
338
                $options['retry_count'],
339 12
                $response
340 12
            );
341
        } else {
342
            $defaultDelayTimeout = (float) $options['default_retry_multiplier'] * $options['retry_count'];
343 45
        }
344
345
        // Retry-After can be a delay in seconds or a date
346
        // (see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
347
        if ($response && $response->hasHeader($options['retry_after_header'])) {
348
            $timeout = $this->deriveTimeoutFromHeader(
349
                $response->getHeader($options['retry_after_header'])[0],
350
                $options['retry_after_date_format']
351
            ) ?: $defaultDelayTimeout;
352
        } else {
353
            $timeout = abs($defaultDelayTimeout);
354 12
        }
355
356
        // If the max_allowable_timeout_secs is set
357
        if (! is_null($options['max_allowable_timeout_secs']) && abs($options['max_allowable_timeout_secs']) > 0) {
358 12
            return min(abs($timeout), (float) abs($options['max_allowable_timeout_secs']));
359 6
        } else {
360 6
            return abs($timeout);
361 3
        }
362
    }
363
364 3
    /**
365
     * Attempt to derive the timeout from the `Retry-After` (or custom) header value
366
     *
367
     * The spec allows the header value to either be a number of seconds or a datetime.
368
     *
369
     * @param string $headerValue
370
     * @param string $dateFormat
371
     * @return float|null  The number of seconds to wait, or NULL if unsuccessful (invalid header)
372
     */
373
    protected function deriveTimeoutFromHeader(string $headerValue, string $dateFormat = self::DATE_FORMAT): ?float
374
    {
375
        // The timeout will either be a number or a HTTP-formatted date,
376
        // or seconds (integer)
377
        if (is_numeric($headerValue)) {
378
            return (float) trim($headerValue);
379
        } elseif ($date = DateTime::createFromFormat($dateFormat ?: self::DATE_FORMAT, trim($headerValue))) {
380
            return (float) $date->format('U') - time();
381
        }
382
383
        return null;
384
    }
385
}
386