Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
namespace Aws\Credentials;
3
 
1441 ariadna 4
use Aws\Arn\Arn;
1 efrain 5
use Aws\Exception\CredentialsException;
1441 ariadna 6
use GuzzleHttp\Exception\ConnectException;
7
use GuzzleHttp\Exception\GuzzleException;
1 efrain 8
use GuzzleHttp\Psr7\Request;
1441 ariadna 9
use GuzzleHttp\Promise;
1 efrain 10
use GuzzleHttp\Promise\PromiseInterface;
11
use Psr\Http\Message\ResponseInterface;
12
 
13
/**
1441 ariadna 14
 * Credential provider that fetches container credentials with GET request.
15
 * container environment variables are used in constructing request URI.
1 efrain 16
 */
17
class EcsCredentialProvider
18
{
19
    const SERVER_URI = 'http://169.254.170.2';
20
    const ENV_URI = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";
21
    const ENV_FULL_URI = "AWS_CONTAINER_CREDENTIALS_FULL_URI";
22
    const ENV_AUTH_TOKEN = "AWS_CONTAINER_AUTHORIZATION_TOKEN";
1441 ariadna 23
    const ENV_AUTH_TOKEN_FILE = "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE";
1 efrain 24
    const ENV_TIMEOUT = 'AWS_METADATA_SERVICE_TIMEOUT';
1441 ariadna 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';
1 efrain 28
 
1441 ariadna 29
    const DEFAULT_ENV_TIMEOUT = 1.0;
30
    const DEFAULT_ENV_RETRIES = 3;
31
 
1 efrain 32
    /** @var callable */
33
    private $client;
34
 
35
    /** @var float|mixed */
36
    private $timeout;
37
 
1441 ariadna 38
    /** @var int */
39
    private $retries;
40
 
41
    /** @var int */
42
    private $attempts;
43
 
1 efrain 44
    /**
45
     *  The constructor accepts following options:
46
     *  - timeout: (optional) Connection timeout, in seconds, default 1.0
1441 ariadna 47
     *  - retries: Optional number of retries to be attempted, default 3.
1 efrain 48
     *  - client: An EcsClient to make request from
49
     *
50
     * @param array $config Configuration options
51
     */
52
    public function __construct(array $config = [])
53
    {
1441 ariadna 54
        $this->timeout = (float) isset($config['timeout'])
55
            ? $config['timeout']
56
            : (getenv(self::ENV_TIMEOUT) ?: self::DEFAULT_ENV_TIMEOUT);
57
        $this->retries = (int) isset($config['retries'])
58
            ? $config['retries']
59
            : ((int) getenv(self::ENV_RETRIES) ?: self::DEFAULT_ENV_RETRIES);
1 efrain 60
 
1441 ariadna 61
        $this->client = $config['client'] ?? \Aws\default_http_handler();
1 efrain 62
    }
63
 
64
    /**
1441 ariadna 65
     * Load container credentials.
1 efrain 66
     *
67
     * @return PromiseInterface
1441 ariadna 68
     * @throws GuzzleException
1 efrain 69
     */
70
    public function __invoke()
71
    {
1441 ariadna 72
        $this->attempts = 0;
73
        $uri = $this->getEcsUri();
74
        if ($this->isCompatibleUri($uri)) {
75
            return Promise\Coroutine::of(function () {
76
                $client = $this->client;
77
                $request = new Request('GET', $this->getEcsUri());
78
                $headers = $this->getHeadersForAuthToken();
79
                $credentials = null;
80
                while ($credentials === null) {
81
                    $credentials = (yield $client(
82
                        $request,
83
                        [
84
                            'timeout' => $this->timeout,
85
                            'proxy' => '',
86
                            'headers' => $headers,
87
                        ]
88
                    )->then(function (ResponseInterface $response) {
89
                        $result = $this->decodeResult((string)$response->getBody());
90
                        if (!isset($result['AccountId']) && isset($result['RoleArn'])) {
91
                            try {
92
                                $parsedArn = new Arn($result['RoleArn']);
93
                                $result['AccountId'] = $parsedArn->getAccountId();
94
                            } catch (\Exception $e) {
95
                                // AccountId will be null
96
                            }
97
                        }
98
 
99
                        return new Credentials(
100
                            $result['AccessKeyId'],
101
                            $result['SecretAccessKey'],
102
                            $result['Token'],
103
                            strtotime($result['Expiration']),
104
                            $result['AccountId'] ?? null,
105
                            CredentialSources::ECS
106
                        );
107
                    })->otherwise(function ($reason) {
108
                        $reason = is_array($reason) ? $reason['exception'] : $reason;
109
 
110
                        $isRetryable = $reason instanceof ConnectException;
111
                        if ($isRetryable && ($this->attempts < $this->retries)) {
112
                            sleep((int)pow(1.2, $this->attempts));
113
                        } else {
114
                            $msg = $reason->getMessage();
115
                            throw new CredentialsException(
116
                                sprintf('Error retrieving credentials from container metadata after attempt %d/%d (%s)', $this->attempts, $this->retries, $msg)
117
                            );
118
                        }
119
                    }));
120
                    $this->attempts++;
121
                }
122
 
123
                yield $credentials;
124
            });
125
        }
126
 
127
        throw new CredentialsException("Uri '{$uri}' contains an unsupported host.");
1 efrain 128
    }
1441 ariadna 129
 
130
    /**
131
     * Returns the number of attempts that have been done.
132
     *
133
     * @return int
134
     */
135
    public function getAttempts(): int
136
    {
137
        return $this->attempts;
138
    }
139
 
140
    /**
141
     * Retrieves authorization token.
142
     *
143
     * @return array|false|string
144
     */
1 efrain 145
    private function getEcsAuthToken()
146
    {
1441 ariadna 147
        if (!empty($path = getenv(self::ENV_AUTH_TOKEN_FILE))) {
148
            $token =  @file_get_contents($path);
149
            if (false === $token) {
150
                clearstatcache(true, dirname($path) . DIRECTORY_SEPARATOR . @readlink($path));
151
                clearstatcache(true, dirname($path) . DIRECTORY_SEPARATOR . dirname(@readlink($path)));
152
                clearstatcache(true, $path);
153
            }
154
 
155
            if (!is_readable($path)) {
156
                throw new CredentialsException("Failed to read authorization token from '{$path}': no such file or directory.");
157
            }
158
 
159
            $token = @file_get_contents($path);
160
 
161
            if (empty($token)) {
162
                throw new CredentialsException("Invalid authorization token read from `$path`. Token file is empty!");
163
            }
164
 
165
            return $token;
166
        }
167
 
1 efrain 168
        return getenv(self::ENV_AUTH_TOKEN);
169
    }
170
 
1441 ariadna 171
    /**
172
     * Provides headers for credential metadata request.
173
     *
174
     * @return array|array[]|string[]
175
     */
176
    private function getHeadersForAuthToken()
177
    {
1 efrain 178
        $authToken = self::getEcsAuthToken();
179
        $headers = [];
1441 ariadna 180
 
181
        if (!empty($authToken))
1 efrain 182
            $headers = ['Authorization' => $authToken];
183
 
184
        return $headers;
185
    }
186
 
1441 ariadna 187
    /** @deprecated */
188
    public function setHeaderForAuthToken()
189
    {
190
        $authToken = self::getEcsAuthToken();
191
        $headers = [];
192
        if (!empty($authToken))
193
            $headers = ['Authorization' => $authToken];
194
 
195
        return $headers;
196
    }
197
 
1 efrain 198
    /**
1441 ariadna 199
     * Fetch container metadata URI from container environment variable.
1 efrain 200
     *
1441 ariadna 201
     * @return string Returns container metadata URI
1 efrain 202
     */
203
    private function getEcsUri()
204
    {
205
        $credsUri = getenv(self::ENV_URI);
206
 
207
        if ($credsUri === false) {
1441 ariadna 208
            $credsUri = $_SERVER[self::ENV_URI] ?? '';
1 efrain 209
        }
210
 
1441 ariadna 211
        if (empty($credsUri)){
1 efrain 212
            $credFullUri = getenv(self::ENV_FULL_URI);
1441 ariadna 213
            if ($credFullUri === false){
214
                $credFullUri = $_SERVER[self::ENV_FULL_URI] ?? '';
1 efrain 215
            }
216
 
1441 ariadna 217
            if (!empty($credFullUri))
1 efrain 218
                return $credFullUri;
219
        }
1441 ariadna 220
 
1 efrain 221
        return self::SERVER_URI . $credsUri;
222
    }
223
 
224
    private function decodeResult($response)
225
    {
226
        $result = json_decode($response, true);
227
 
228
        if (!isset($result['AccessKeyId'])) {
1441 ariadna 229
            throw new CredentialsException('Unexpected container metadata credentials value');
1 efrain 230
        }
231
        return $result;
232
    }
1441 ariadna 233
 
234
    /**
235
     * Determines whether or not a given request URI is a valid
236
     * container credential request URI.
237
     *
238
     * @param $uri
239
     *
240
     * @return bool
241
     */
242
    private function isCompatibleUri($uri)
243
    {
244
        $parsed = parse_url($uri);
245
 
246
        if ($parsed['scheme'] !== 'https') {
247
            $host = trim($parsed['host'], '[]');
248
            $ecsHost = parse_url(self::SERVER_URI)['host'];
249
            $eksHost = self::EKS_SERVER_HOST_IPV4;
250
 
251
            if ($host !== $ecsHost
252
                && $host !== $eksHost
253
                && $host !== self::EKS_SERVER_HOST_IPV6
254
                && !CredentialsUtils::isLoopBackAddress(gethostbyname($host))
255
            ) {
256
                return false;
257
            }
258
        }
259
 
260
        return true;
261
    }
1 efrain 262
}