Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
namespace Aws\Credentials;
3
 
4
use Aws\Exception\CredentialsException;
5
use Aws\Exception\InvalidJsonException;
6
use Aws\Sdk;
7
use GuzzleHttp\Exception\TransferException;
8
use GuzzleHttp\Promise;
9
use GuzzleHttp\Exception\RequestException;
10
use GuzzleHttp\Psr7\Request;
11
use GuzzleHttp\Promise\PromiseInterface;
12
use Psr\Http\Message\ResponseInterface;
13
 
14
/**
15
 * Credential provider that provides credentials from the EC2 metadata service.
16
 */
17
class InstanceProfileProvider
18
{
19
    const SERVER_URI = 'http://169.254.169.254/latest/';
20
    const CRED_PATH = 'meta-data/iam/security-credentials/';
21
    const TOKEN_PATH = 'api/token';
22
 
23
    const ENV_DISABLE = 'AWS_EC2_METADATA_DISABLED';
24
    const ENV_TIMEOUT = 'AWS_METADATA_SERVICE_TIMEOUT';
25
    const ENV_RETRIES = 'AWS_METADATA_SERVICE_NUM_ATTEMPTS';
26
 
27
    /** @var string */
28
    private $profile;
29
 
30
    /** @var callable */
31
    private $client;
32
 
33
    /** @var int */
34
    private $retries;
35
 
36
    /** @var int */
37
    private $attempts;
38
 
39
    /** @var float|mixed */
40
    private $timeout;
41
 
42
    /** @var bool */
43
    private $secureMode = true;
44
 
45
    /**
46
     * The constructor accepts the following options:
47
     *
48
     * - timeout: Connection timeout, in seconds.
49
     * - profile: Optional EC2 profile name, if known.
50
     * - retries: Optional number of retries to be attempted.
51
     *
52
     * @param array $config Configuration options.
53
     */
54
    public function __construct(array $config = [])
55
    {
56
        $this->timeout = (float) getenv(self::ENV_TIMEOUT) ?: (isset($config['timeout']) ? $config['timeout'] : 1.0);
57
        $this->profile = isset($config['profile']) ? $config['profile'] : null;
58
        $this->retries = (int) getenv(self::ENV_RETRIES) ?: (isset($config['retries']) ? $config['retries'] : 3);
59
        $this->client = isset($config['client'])
60
            ? $config['client'] // internal use only
61
            : \Aws\default_http_handler();
62
    }
63
 
64
    /**
65
     * Loads instance profile credentials.
66
     *
67
     * @return PromiseInterface
68
     */
69
    public function __invoke($previousCredentials = null)
70
    {
71
        $this->attempts = 0;
72
        return Promise\Coroutine::of(function () use ($previousCredentials) {
73
 
74
            // Retrieve token or switch out of secure mode
75
            $token = null;
76
            while ($this->secureMode && is_null($token)) {
77
                try {
78
                    $token = (yield $this->request(
79
                        self::TOKEN_PATH,
80
                        'PUT',
81
                        [
82
                            'x-aws-ec2-metadata-token-ttl-seconds' => 21600
83
                        ]
84
                    ));
85
                } catch (TransferException $e) {
86
                    if ($this->getExceptionStatusCode($e) === 500
87
                        && $previousCredentials instanceof Credentials
88
                    ) {
89
                        goto generateCredentials;
90
                    }
91
                    else if (!method_exists($e, 'getResponse')
92
                        || empty($e->getResponse())
93
                        || !in_array(
94
                            $e->getResponse()->getStatusCode(),
95
                            [400, 500, 502, 503, 504]
96
                        )
97
                    ) {
98
                        $this->secureMode = false;
99
                    } else {
100
                        $this->handleRetryableException(
101
                            $e,
102
                            [],
103
                            $this->createErrorMessage(
104
                                'Error retrieving metadata token'
105
                            )
106
                        );
107
                    }
108
                }
109
                $this->attempts++;
110
            }
111
 
112
            // Set token header only for secure mode
113
            $headers = [];
114
            if ($this->secureMode) {
115
                $headers = [
116
                    'x-aws-ec2-metadata-token' => $token
117
                ];
118
            }
119
 
120
            // Retrieve profile
121
            while (!$this->profile) {
122
                try {
123
                    $this->profile = (yield $this->request(
124
                        self::CRED_PATH,
125
                        'GET',
126
                        $headers
127
                    ));
128
                } catch (TransferException $e) {
129
                    // 401 indicates insecure flow not supported, switch to
130
                    // attempting secure mode for subsequent calls
131
                    if (!empty($this->getExceptionStatusCode($e))
132
                        && $this->getExceptionStatusCode($e) === 401
133
                    ) {
134
                        $this->secureMode = true;
135
                    }
136
                    $this->handleRetryableException(
137
                        $e,
138
                        [ 'blacklist' => [401, 403] ],
139
                        $this->createErrorMessage($e->getMessage())
140
                    );
141
                }
142
 
143
                $this->attempts++;
144
            }
145
 
146
            // Retrieve credentials
147
            $result = null;
148
            while ($result == null) {
149
                try {
150
                    $json = (yield $this->request(
151
                        self::CRED_PATH . $this->profile,
152
                        'GET',
153
                        $headers
154
                    ));
155
                    $result = $this->decodeResult($json);
156
                } catch (InvalidJsonException $e) {
157
                    $this->handleRetryableException(
158
                        $e,
159
                        [ 'blacklist' => [401, 403] ],
160
                        $this->createErrorMessage(
161
                            'Invalid JSON response, retries exhausted'
162
                        )
163
                    );
164
                } catch (TransferException $e) {
165
                    // 401 indicates insecure flow not supported, switch to
166
                    // attempting secure mode for subsequent calls
167
                    if (($this->getExceptionStatusCode($e) === 500
168
                            || strpos($e->getMessage(), "cURL error 28") !== false)
169
                        && $previousCredentials instanceof Credentials
170
                    ) {
171
                        goto generateCredentials;
172
                    } else if (!empty($this->getExceptionStatusCode($e))
173
                        && $this->getExceptionStatusCode($e) === 401
174
                    ) {
175
                        $this->secureMode = true;
176
                    }
177
                    $this->handleRetryableException(
178
                        $e,
179
                        [ 'blacklist' => [401, 403] ],
180
                        $this->createErrorMessage($e->getMessage())
181
                    );
182
                }
183
                $this->attempts++;
184
            }
185
            generateCredentials:
186
 
187
            if (!isset($result)) {
188
                $credentials = $previousCredentials;
189
            } else {
190
                $credentials = new Credentials(
191
                    $result['AccessKeyId'],
192
                    $result['SecretAccessKey'],
193
                    $result['Token'],
194
                    strtotime($result['Expiration'])
195
                );
196
            }
197
 
198
            if ($credentials->isExpired()) {
199
                $credentials->extendExpiration();
200
            }
201
 
202
            yield $credentials;
203
        });
204
    }
205
 
206
    /**
207
     * @param string $url
208
     * @param string $method
209
     * @param array $headers
210
     * @return PromiseInterface Returns a promise that is fulfilled with the
211
     *                          body of the response as a string.
212
     */
213
    private function request($url, $method = 'GET', $headers = [])
214
    {
215
        $disabled = getenv(self::ENV_DISABLE) ?: false;
216
        if (strcasecmp($disabled, 'true') === 0) {
217
            throw new CredentialsException(
218
                $this->createErrorMessage('EC2 metadata service access disabled')
219
            );
220
        }
221
 
222
        $fn = $this->client;
223
        $request = new Request($method, self::SERVER_URI . $url);
224
        $userAgent = 'aws-sdk-php/' . Sdk::VERSION;
225
        if (defined('HHVM_VERSION')) {
226
            $userAgent .= ' HHVM/' . HHVM_VERSION;
227
        }
228
        $userAgent .= ' ' . \Aws\default_user_agent();
229
        $request = $request->withHeader('User-Agent', $userAgent);
230
        foreach ($headers as $key => $value) {
231
            $request = $request->withHeader($key, $value);
232
        }
233
 
234
        return $fn($request, ['timeout' => $this->timeout])
235
            ->then(function (ResponseInterface $response) {
236
                return (string) $response->getBody();
237
            })->otherwise(function (array $reason) {
238
                $reason = $reason['exception'];
239
                if ($reason instanceof TransferException) {
240
                    throw $reason;
241
                }
242
                $msg = $reason->getMessage();
243
                throw new CredentialsException(
244
                    $this->createErrorMessage($msg)
245
                );
246
            });
247
    }
248
 
249
    private function handleRetryableException(
250
        \Exception $e,
251
        $retryOptions,
252
        $message
253
    ) {
254
        $isRetryable = true;
255
        if (!empty($status = $this->getExceptionStatusCode($e))
256
            && isset($retryOptions['blacklist'])
257
            && in_array($status, $retryOptions['blacklist'])
258
        ) {
259
            $isRetryable = false;
260
        }
261
        if ($isRetryable && $this->attempts < $this->retries) {
262
            sleep((int) pow(1.2, $this->attempts));
263
        } else {
264
            throw new CredentialsException($message);
265
        }
266
    }
267
 
268
    private function getExceptionStatusCode(\Exception $e)
269
    {
270
        if (method_exists($e, 'getResponse')
271
            && !empty($e->getResponse())
272
        ) {
273
            return $e->getResponse()->getStatusCode();
274
        }
275
        return null;
276
    }
277
 
278
    private function createErrorMessage($previous)
279
    {
280
        return "Error retrieving credentials from the instance profile "
281
            . "metadata service. ({$previous})";
282
    }
283
 
284
    private function decodeResult($response)
285
    {
286
        $result = json_decode($response, true);
287
 
288
        if (json_last_error() > 0) {
289
            throw new InvalidJsonException();
290
        }
291
 
292
        if ($result['Code'] !== 'Success') {
293
            throw new CredentialsException('Unexpected instance profile '
294
                .  'response code: ' . $result['Code']);
295
        }
296
 
297
        return $result;
298
    }
299
}