PluginProbe ʕ •ᴥ•ʔ
Transferito: WP Migration / trunk
Transferito: WP Migration vtrunk
trunk 11.4.0 12.0.0 13.1.0 14.0.0 14.0.11 14.0.7 14.1.0 14.1.1 14.1.2 14.1.3 14.1.4
transferito / vendor / aws / aws-sdk-php / src / Credentials / InstanceProfileProvider.php
transferito / vendor / aws / aws-sdk-php / src / Credentials Last commit date
AssumeRoleCredentialProvider.php 11 months ago AssumeRoleWithWebIdentityCredentialProvider.php 11 months ago CredentialProvider.php 11 months ago Credentials.php 11 months ago CredentialsInterface.php 11 months ago EcsCredentialProvider.php 11 months ago InstanceProfileProvider.php 11 months ago
InstanceProfileProvider.php
333 lines
1 <?php
2 namespace Aws\Credentials;
3
4 use Aws\Configuration\ConfigurationResolver;
5 use Aws\Exception\CredentialsException;
6 use Aws\Exception\InvalidJsonException;
7 use Aws\Sdk;
8 use GuzzleHttp\Exception\TransferException;
9 use GuzzleHttp\Promise;
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 const ENV_DISABLE = 'AWS_EC2_METADATA_DISABLED';
23 const ENV_TIMEOUT = 'AWS_METADATA_SERVICE_TIMEOUT';
24 const ENV_RETRIES = 'AWS_METADATA_SERVICE_NUM_ATTEMPTS';
25 const CFG_EC2_METADATA_V1_DISABLED = 'ec2_metadata_v1_disabled';
26 const DEFAULT_TIMEOUT = 1.0;
27 const DEFAULT_RETRIES = 3;
28 const DEFAULT_TOKEN_TTL_SECONDS = 21600;
29 const DEFAULT_AWS_EC2_METADATA_V1_DISABLED = false;
30
31 /** @var string */
32 private $profile;
33
34 /** @var callable */
35 private $client;
36
37 /** @var int */
38 private $retries;
39
40 /** @var int */
41 private $attempts;
42
43 /** @var float|mixed */
44 private $timeout;
45
46 /** @var bool */
47 private $secureMode = true;
48
49 /** @var bool|null */
50 private $ec2MetadataV1Disabled;
51
52 /**
53 * The constructor accepts the following options:
54 *
55 * - timeout: Connection timeout, in seconds.
56 * - profile: Optional EC2 profile name, if known.
57 * - retries: Optional number of retries to be attempted.
58 * - ec2_metadata_v1_disabled: Optional for disabling the fallback to IMDSv1.
59 *
60 * @param array $config Configuration options.
61 */
62 public function __construct(array $config = [])
63 {
64 $this->timeout = (float) getenv(self::ENV_TIMEOUT) ?: ($config['timeout'] ?? self::DEFAULT_TIMEOUT);
65 $this->profile = $config['profile'] ?? null;
66 $this->retries = (int) getenv(self::ENV_RETRIES) ?: ($config['retries'] ?? self::DEFAULT_RETRIES);
67 $this->client = $config['client'] ?? \Aws\default_http_handler();
68 $this->ec2MetadataV1Disabled = $config[self::CFG_EC2_METADATA_V1_DISABLED] ?? null;
69 }
70
71 /**
72 * Loads instance profile credentials.
73 *
74 * @return PromiseInterface
75 */
76 public function __invoke($previousCredentials = null)
77 {
78 $this->attempts = 0;
79 return Promise\Coroutine::of(function () use ($previousCredentials) {
80
81 // Retrieve token or switch out of secure mode
82 $token = null;
83 while ($this->secureMode && is_null($token)) {
84 try {
85 $token = (yield $this->request(
86 self::TOKEN_PATH,
87 'PUT',
88 [
89 'x-aws-ec2-metadata-token-ttl-seconds' => self::DEFAULT_TOKEN_TTL_SECONDS
90 ]
91 ));
92 } catch (TransferException $e) {
93 if ($this->getExceptionStatusCode($e) === 500
94 && $previousCredentials instanceof Credentials
95 ) {
96 goto generateCredentials;
97 } elseif ($this->shouldFallbackToIMDSv1()
98 && (!method_exists($e, 'getResponse')
99 || empty($e->getResponse())
100 || !in_array(
101 $e->getResponse()->getStatusCode(),
102 [400, 500, 502, 503, 504]
103 ))
104 ) {
105 $this->secureMode = false;
106 } else {
107 $this->handleRetryableException(
108 $e,
109 [],
110 $this->createErrorMessage(
111 'Error retrieving metadata token'
112 )
113 );
114 }
115 }
116 $this->attempts++;
117 }
118
119 // Set token header only for secure mode
120 $headers = [];
121 if ($this->secureMode) {
122 $headers = [
123 'x-aws-ec2-metadata-token' => $token
124 ];
125 }
126
127 // Retrieve profile
128 while (!$this->profile) {
129 try {
130 $this->profile = (yield $this->request(
131 self::CRED_PATH,
132 'GET',
133 $headers
134 ));
135 } catch (TransferException $e) {
136 // 401 indicates insecure flow not supported, switch to
137 // attempting secure mode for subsequent calls
138 if (!empty($this->getExceptionStatusCode($e))
139 && $this->getExceptionStatusCode($e) === 401
140 ) {
141 $this->secureMode = true;
142 }
143 $this->handleRetryableException(
144 $e,
145 [ 'blacklist' => [401, 403] ],
146 $this->createErrorMessage($e->getMessage())
147 );
148 }
149
150 $this->attempts++;
151 }
152
153 // Retrieve credentials
154 $result = null;
155 while ($result == null) {
156 try {
157 $json = (yield $this->request(
158 self::CRED_PATH . $this->profile,
159 'GET',
160 $headers
161 ));
162 $result = $this->decodeResult($json);
163 } catch (InvalidJsonException $e) {
164 $this->handleRetryableException(
165 $e,
166 [ 'blacklist' => [401, 403] ],
167 $this->createErrorMessage(
168 'Invalid JSON response, retries exhausted'
169 )
170 );
171 } catch (TransferException $e) {
172 // 401 indicates insecure flow not supported, switch to
173 // attempting secure mode for subsequent calls
174 if (($this->getExceptionStatusCode($e) === 500
175 || strpos($e->getMessage(), "cURL error 28") !== false)
176 && $previousCredentials instanceof Credentials
177 ) {
178 goto generateCredentials;
179 } elseif (!empty($this->getExceptionStatusCode($e))
180 && $this->getExceptionStatusCode($e) === 401
181 ) {
182 $this->secureMode = true;
183 }
184 $this->handleRetryableException(
185 $e,
186 [ 'blacklist' => [401, 403] ],
187 $this->createErrorMessage($e->getMessage())
188 );
189 }
190 $this->attempts++;
191 }
192 generateCredentials:
193
194 if (!isset($result)) {
195 $credentials = $previousCredentials;
196 } else {
197 $credentials = new Credentials(
198 $result['AccessKeyId'],
199 $result['SecretAccessKey'],
200 $result['Token'],
201 strtotime($result['Expiration'])
202 );
203 }
204
205 if ($credentials->isExpired()) {
206 $credentials->extendExpiration();
207 }
208
209 yield $credentials;
210 });
211 }
212
213 /**
214 * @param string $url
215 * @param string $method
216 * @param array $headers
217 * @return PromiseInterface Returns a promise that is fulfilled with the
218 * body of the response as a string.
219 */
220 private function request($url, $method = 'GET', $headers = [])
221 {
222 $disabled = getenv(self::ENV_DISABLE) ?: false;
223 if (strcasecmp($disabled, 'true') === 0) {
224 throw new CredentialsException(
225 $this->createErrorMessage('EC2 metadata service access disabled')
226 );
227 }
228
229 $fn = $this->client;
230 $request = new Request($method, self::SERVER_URI . $url);
231 $userAgent = 'aws-sdk-php/' . Sdk::VERSION;
232 if (defined('HHVM_VERSION')) {
233 $userAgent .= ' HHVM/' . HHVM_VERSION;
234 }
235 $userAgent .= ' ' . \Aws\default_user_agent();
236 $request = $request->withHeader('User-Agent', $userAgent);
237 foreach ($headers as $key => $value) {
238 $request = $request->withHeader($key, $value);
239 }
240
241 return $fn($request, ['timeout' => $this->timeout])
242 ->then(function (ResponseInterface $response) {
243 return (string) $response->getBody();
244 })->otherwise(function (array $reason) {
245 $reason = $reason['exception'];
246 if ($reason instanceof TransferException) {
247 throw $reason;
248 }
249 $msg = $reason->getMessage();
250 throw new CredentialsException(
251 $this->createErrorMessage($msg)
252 );
253 });
254 }
255
256 private function handleRetryableException(
257 \Exception $e,
258 $retryOptions,
259 $message
260 ) {
261 $isRetryable = true;
262 if (!empty($status = $this->getExceptionStatusCode($e))
263 && isset($retryOptions['blacklist'])
264 && in_array($status, $retryOptions['blacklist'])
265 ) {
266 $isRetryable = false;
267 }
268 if ($isRetryable && $this->attempts < $this->retries) {
269 sleep((int) pow(1.2, $this->attempts));
270 } else {
271 throw new CredentialsException($message);
272 }
273 }
274
275 private function getExceptionStatusCode(\Exception $e)
276 {
277 if (method_exists($e, 'getResponse')
278 && !empty($e->getResponse())
279 ) {
280 return $e->getResponse()->getStatusCode();
281 }
282 return null;
283 }
284
285 private function createErrorMessage($previous)
286 {
287 return "Error retrieving credentials from the instance profile "
288 . "metadata service. ({$previous})";
289 }
290
291 private function decodeResult($response)
292 {
293 $result = json_decode($response, true);
294
295 if (json_last_error() > 0) {
296 throw new InvalidJsonException();
297 }
298
299 if ($result['Code'] !== 'Success') {
300 throw new CredentialsException('Unexpected instance profile '
301 . 'response code: ' . $result['Code']);
302 }
303
304 return $result;
305 }
306
307 /**
308 * This functions checks for whether we should fall back to IMDSv1 or not.
309 * If $ec2MetadataV1Disabled is null then we will try to resolve this value from
310 * the following sources:
311 * - From environment: "AWS_EC2_METADATA_V1_DISABLED".
312 * - From config file: aws_ec2_metadata_v1_disabled
313 * - Defaulted to false
314 *
315 * @return bool
316 */
317 private function shouldFallbackToIMDSv1(): bool
318 {
319 $isImdsV1Disabled = \Aws\boolean_value($this->ec2MetadataV1Disabled)
320 ?? \Aws\boolean_value(
321 ConfigurationResolver::resolve(
322 self::CFG_EC2_METADATA_V1_DISABLED,
323 self::DEFAULT_AWS_EC2_METADATA_V1_DISABLED,
324 'bool',
325 ['use_aws_shared_config_files' => true]
326 )
327 )
328 ?? self::DEFAULT_AWS_EC2_METADATA_V1_DISABLED;
329
330 return !$isImdsV1Disabled;
331 }
332 }
333