PluginProbe
Media Cloud Sync / 1.4.2
Media Cloud Sync v1.4.2
1.4.2 1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 All 36 releases
← All changes | includes/sdk/s3/Aws/Credentials/EcsCredentialProvider.php +129 -25 1.2.7 → 1.4.2 View file →
@@ -1,15 +1,19 @@
1 1 <?php
2 2
3 3 namespace Dudlewebs\WPMCS\s3\Aws\Credentials;
4 4
5 +use Dudlewebs\WPMCS\s3\Aws\Arn\Arn;
5 6 use Dudlewebs\WPMCS\s3\Aws\Exception\CredentialsException;
7 +use Dudlewebs\WPMCS\s3\GuzzleHttp\Exception\ConnectException;
8 +use Dudlewebs\WPMCS\s3\GuzzleHttp\Exception\GuzzleException;
6 9 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Request;
10 +use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise;
7 11 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise\PromiseInterface;
8 12 use Dudlewebs\WPMCS\s3\Psr\Http\Message\ResponseInterface;
9 13 /**
10 - * Credential provider that fetches credentials with GET request.
11 - * ECS environment variable is used in constructing request URI.
14 + * Credential provider that fetches container credentials with GET request.
15 + * container environment variables are used in constructing request URI.
12 16 */
13 17 class EcsCredentialProvider
14 18 {
15 19 const SERVER_URI = 'http://169.254.170.2';
@@ -15,16 +19,27 @@
15 19 const SERVER_URI = 'http://169.254.170.2';
16 20 const ENV_URI = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";
17 21 const ENV_FULL_URI = "AWS_CONTAINER_CREDENTIALS_FULL_URI";
18 22 const ENV_AUTH_TOKEN = "AWS_CONTAINER_AUTHORIZATION_TOKEN";
23 + const ENV_AUTH_TOKEN_FILE = "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE";
19 24 const ENV_TIMEOUT = 'AWS_METADATA_SERVICE_TIMEOUT';
25 + const EKS_SERVER_HOST_IPV4 = '169.254.170.23';
26 + const EKS_SERVER_HOST_IPV6 = 'fd00:ec2::23';
27 + const ENV_RETRIES = 'AWS_METADATA_SERVICE_NUM_ATTEMPTS';
28 + const DEFAULT_ENV_TIMEOUT = 1.0;
29 + const DEFAULT_ENV_RETRIES = 3;
20 30 /** @var callable */
21 31 private $client;
22 32 /** @var float|mixed */
23 33 private $timeout;
34 + /** @var int */
35 + private $retries;
36 + /** @var int */
37 + private $attempts;
24 38 /**
25 39 * The constructor accepts following options:
26 40 * - timeout: (optional) Connection timeout, in seconds, default 1.0
41 + * - retries: Optional number of retries to be attempted, default 3.
27 42 * - client: An EcsClient to make request from
28 43 *
29 44 * @param array $config Configuration options
30 45 */
@@ -29,38 +44,106 @@
29 44 * @param array $config Configuration options
30 45 */
31 46 public function __construct(array $config = [])
32 47 {
33 - $timeout = \getenv(self::ENV_TIMEOUT);
34 - if (!$timeout) {
35 - $timeout = isset($_SERVER[self::ENV_TIMEOUT]) ? $_SERVER[self::ENV_TIMEOUT] : (isset($config['timeout']) ? $config['timeout'] : 1.0);
36 - }
37 - $this->timeout = (float) $timeout;
38 - $this->client = isset($config['client']) ? $config['client'] : \Dudlewebs\WPMCS\s3\Aws\default_http_handler();
48 + $this->timeout = (float) isset($config['timeout']) ? $config['timeout'] : (\getenv(self::ENV_TIMEOUT) ?: self::DEFAULT_ENV_TIMEOUT);
49 + $this->retries = (int) isset($config['retries']) ? $config['retries'] : ((int) \getenv(self::ENV_RETRIES) ?: self::DEFAULT_ENV_RETRIES);
50 + $this->client = $config['client'] ?? \Dudlewebs\WPMCS\s3\Aws\default_http_handler();
39 51 }
40 52 /**
41 - * Load ECS credentials
53 + * Load container credentials.
42 54 *
43 55 * @return PromiseInterface
56 + * @throws GuzzleException
44 57 */
45 58 public function __invoke()
46 59 {
47 - $client = $this->client;
48 - $request = new Request('GET', self::getEcsUri());
49 - $headers = $this->setHeaderForAuthToken();
50 - return $client($request, ['timeout' => $this->timeout, 'proxy' => '', 'headers' => $headers])->then(function (ResponseInterface $response) {
51 - $result = $this->decodeResult((string) $response->getBody());
52 - return new Credentials($result['AccessKeyId'], $result['SecretAccessKey'], $result['Token'], \strtotime($result['Expiration']));
53 - })->otherwise(function ($reason) {
54 - $reason = \is_array($reason) ? $reason['exception'] : $reason;
55 - $msg = $reason->getMessage();
56 - throw new CredentialsException("Error retrieving credential from ECS ({$msg})");
57 - });
60 + $this->attempts = 0;
61 + $uri = $this->getEcsUri();
62 + if ($this->isCompatibleUri($uri)) {
63 + return Promise\Coroutine::of(function () {
64 + $client = $this->client;
65 + $request = new Request('GET', $this->getEcsUri());
66 + $headers = $this->getHeadersForAuthToken();
67 + $credentials = null;
68 + while ($credentials === null) {
69 + $credentials = (yield $client($request, ['timeout' => $this->timeout, 'proxy' => '', 'headers' => $headers])->then(function (ResponseInterface $response) {
70 + $result = $this->decodeResult((string) $response->getBody());
71 + if (!isset($result['AccountId']) && isset($result['RoleArn'])) {
72 + try {
73 + $parsedArn = new Arn($result['RoleArn']);
74 + $result['AccountId'] = $parsedArn->getAccountId();
75 + } catch (\Exception $e) {
76 + // AccountId will be null
77 + }
78 + }
79 + return new Credentials($result['AccessKeyId'], $result['SecretAccessKey'], $result['Token'], \strtotime($result['Expiration']), $result['AccountId'] ?? null, CredentialSources::ECS);
80 + })->otherwise(function ($reason) {
81 + $reason = \is_array($reason) ? $reason['exception'] : $reason;
82 + $isRetryable = $reason instanceof ConnectException;
83 + if ($isRetryable && $this->attempts < $this->retries) {
84 + \sleep((int) \pow(1.2, $this->attempts));
85 + } else {
86 + $msg = $reason->getMessage();
87 + throw new CredentialsException(\sprintf('Error retrieving credentials from container metadata after attempt %d/%d (%s)', $this->attempts, $this->retries, $msg));
88 + }
89 + }));
90 + $this->attempts++;
91 + }
92 + (yield $credentials);
93 + });
94 + }
95 + throw new CredentialsException("Uri '{$uri}' contains an unsupported host.");
58 96 }
97 + /**
98 + * Returns the number of attempts that have been done.
99 + *
100 + * @return int
101 + */
102 + public function getAttempts() : int
103 + {
104 + return $this->attempts;
105 + }
106 + /**
107 + * Retrieves authorization token.
108 + *
109 + * @return array|false|string
110 + */
59 111 private function getEcsAuthToken()
60 112 {
113 + if (!empty($path = \getenv(self::ENV_AUTH_TOKEN_FILE))) {
114 + $token = @\file_get_contents($path);
115 + if (\false === $token) {
116 + \clearstatcache(\true, \dirname($path) . \DIRECTORY_SEPARATOR . @\readlink($path));
117 + \clearstatcache(\true, \dirname($path) . \DIRECTORY_SEPARATOR . \dirname(@\readlink($path)));
118 + \clearstatcache(\true, $path);
119 + }
120 + if (!\is_readable($path)) {
121 + throw new CredentialsException("Failed to read authorization token from '{$path}': no such file or directory.");
122 + }
123 + $token = @\file_get_contents($path);
124 + if (empty($token)) {
125 + throw new CredentialsException("Invalid authorization token read from `{$path}`. Token file is empty!");
126 + }
127 + return $token;
128 + }
61 129 return \getenv(self::ENV_AUTH_TOKEN);
62 130 }
131 + /**
132 + * Provides headers for credential metadata request.
133 + *
134 + * @return array|array[]|string[]
135 + */
136 + private function getHeadersForAuthToken()
137 + {
138 + $authToken = self::getEcsAuthToken();
139 + $headers = [];
140 + if (!empty($authToken)) {
141 + $headers = ['Authorization' => $authToken];
142 + }
143 + return $headers;
144 + }
145 + /** @deprecated */
63 146 public function setHeaderForAuthToken()
64 147 {
65 148 $authToken = self::getEcsAuthToken();
66 149 $headers = [];
@@ -69,22 +152,22 @@
69 152 }
70 153 return $headers;
71 154 }
72 155 /**
73 - * Fetch credential URI from ECS environment variable
156 + * Fetch container metadata URI from container environment variable.
74 157 *
75 - * @return string Returns ECS URI
158 + * @return string Returns container metadata URI
76 159 */
77 160 private function getEcsUri()
78 161 {
79 162 $credsUri = \getenv(self::ENV_URI);
80 163 if ($credsUri === \false) {
81 - $credsUri = isset($_SERVER[self::ENV_URI]) ? $_SERVER[self::ENV_URI] : '';
164 + $credsUri = $_SERVER[self::ENV_URI] ?? '';
82 165 }
83 166 if (empty($credsUri)) {
84 167 $credFullUri = \getenv(self::ENV_FULL_URI);
85 168 if ($credFullUri === \false) {
86 - $credFullUri = isset($_SERVER[self::ENV_FULL_URI]) ? $_SERVER[self::ENV_FULL_URI] : '';
169 + $credFullUri = $_SERVER[self::ENV_FULL_URI] ?? '';
87 170 }
88 171 if (!empty($credFullUri)) {
89 172 return $credFullUri;
90 173 }
@@ -94,9 +177,30 @@
94 177 private function decodeResult($response)
95 178 {
96 179 $result = \json_decode($response, \true);
97 180 if (!isset($result['AccessKeyId'])) {
98 - throw new CredentialsException('Unexpected ECS credential value');
181 + throw new CredentialsException('Unexpected container metadata credentials value');
99 182 }
100 183 return $result;
184 + }
185 + /**
186 + * Determines whether or not a given request URI is a valid
187 + * container credential request URI.
188 + *
189 + * @param $uri
190 + *
191 + * @return bool
192 + */
193 + private function isCompatibleUri($uri)
194 + {
195 + $parsed = \parse_url($uri);
196 + if ($parsed['scheme'] !== 'https') {
197 + $host = \trim($parsed['host'], '[]');
198 + $ecsHost = \parse_url(self::SERVER_URI)['host'];
199 + $eksHost = self::EKS_SERVER_HOST_IPV4;
200 + if ($host !== $ecsHost && $host !== $eksHost && $host !== self::EKS_SERVER_HOST_IPV6 && !CredentialsUtils::isLoopBackAddress(\gethostbyname($host))) {
201 + return \false;
202 + }
203 + }
204 + return \true;
101 205 }
102 206 }